diff --git a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls index c0d3aabc80..3e30d7a249 100644 --- a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls +++ b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls @@ -359,7 +359,11 @@ type Agreement { """ The cost details of this agreement on today's date or the date it starts """ - cost: ItemCost! + cost: ItemCost! @deprecated(reason: "Use itemCost instead") + """ + The cost details of this agreement on today's date or the date it starts + """ + itemCost: ItemCost """ The date this agreement took effect. """ @@ -999,7 +1003,7 @@ type ClaimIntentStep { """ A union of all the different kinds of "step content". """ -union ClaimIntentStepContent = ClaimIntentStepContentForm|ClaimIntentStepContentSelect|ClaimIntentStepContentTask|ClaimIntentStepContentAudioRecording|ClaimIntentStepContentFileUpload|ClaimIntentStepContentSummary|ClaimIntentStepContentDeflection +union ClaimIntentStepContent = ClaimIntentStepContentForm|ClaimIntentStepContentSelect|ClaimIntentStepContentTask|ClaimIntentStepContentAudioRecording|ClaimIntentStepContentFileUpload|ClaimIntentStepContentSummary|ClaimIntentStepContentDeflection|ClaimIntentStepContentDeflectionMessage """ An audio recording step is one where the user is meant to record some audio. Submitted using `Mutation.claimIntentSubmitAudio`. @@ -1041,6 +1045,13 @@ type ClaimIntentStepContentDeflectionInfoBlock { title: String! description: String! } +""" +A minimal variant of a deflection step which only carries a text message - no button. +This is a terminal step - and cannot be submitted. +""" +type ClaimIntentStepContentDeflectionMessage { + message: String! +} type ClaimIntentStepContentDeflectionPartner { id: ID! imageUrl: Url @@ -2894,7 +2905,7 @@ type MemberPaymentChargeMethodInfo { """ dueDate: Int """ - Payment provider used for charging the member, eg Trustly or Kivra. + Payment provider used for charging the member, eg Trustly or Invoice. This is used to determine the charge schedule and mandate provider. """ paymentProvider: String! @@ -2939,7 +2950,7 @@ type MemberPaymentInformation { } type MemberPaymentMethod { """ - Payment provider, eg Trustly, Swish, Nordea, Kivra etc. + Payment provider, eg Trustly, Swish, Nordea, Invoice etc. This is used as the "identifier" of the payment method since there can only be one ACTIVE or PENDING payment method per provider. """ @@ -3015,6 +3026,15 @@ type MemberPaymentMethods { The date of the month when member's premium will be charged (or due if invoice) """ chargingDay: Int + """ + Indicates which payment connection the member is missing, if any. The rule is: + - If member has only QASA-paid agreements, the payin connection is irrelevant and only payout is checked. + Returns PAYOUT if no default payout method exists, else null. + - Otherwise (any non-QASA agreement, e.g. member-paid), payin takes priority: + Returns PAYIN if no default payin method exists, else PAYOUT if no default payout method exists, else null. + - If member has no ongoing (active or future) agreements, returns null. + """ + missingConnection: MissingPaymentConnection } enum MemberPaymentMethodStatus { """ @@ -3159,6 +3179,10 @@ type MidtermChangePriceDetailItem { displayName: String! displayValue: String! } +enum MissingPaymentConnection { + PAYIN + PAYOUT +} """ A monetary value with currency. """ @@ -3725,7 +3749,7 @@ type Mutation { Optional `attribution` is merged field-by-field over `ShopSession.attribution` for this confirm only (quotes and `PriceIntentConfirmed` events use the merged result). """ - priceIntentConfirm(priceIntentId: UUID!, attribution: PriceIntentConfirmAttributionInput): PriceIntentMutationOutput! + priceIntentConfirm(priceIntentId: UUID!, attribution: PriceIntentConfirmAttributionInput, generateRecommendations: Boolean): PriceIntentMutationOutput! """ Change the start date of the given `ProductOffer`s by their ID. This is used because it's common to want to change the start date AFTER getting the offer. @@ -3968,7 +3992,11 @@ type PendingContract { """ Calculated based on the pending agreement """ - cost: ItemCost! + cost: ItemCost! @deprecated(reason: "Use itemCost instead") + """ + Calculated based on the pending agreement + """ + itemCost: ItemCost discountsDetails: ContractDiscountDetails! """ Same as `Contract.exposureDisplayName`. diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/MainActivity.kt b/app/app/src/main/kotlin/com/hedvig/android/app/MainActivity.kt index cbc504f88b..2d4d11d53e 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/MainActivity.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/MainActivity.kt @@ -41,7 +41,6 @@ import com.hedvig.android.core.buildconstants.HedvigBuildConstants import com.hedvig.android.core.demomode.DemoManager import com.hedvig.android.core.demomode.Provider import com.hedvig.android.core.rive.RiveInitializer -import com.hedvig.android.data.paying.member.GetOnlyHasNonPayingContractsUseCase import com.hedvig.android.data.settings.datastore.SettingsDataStore import com.hedvig.android.featureflags.FeatureManager import com.hedvig.android.language.LanguageLaunchCheckUseCase @@ -70,9 +69,6 @@ class MainActivity : AppCompatActivity() { @Inject private lateinit var featureManager: FeatureManager - @Inject - private lateinit var getOnlyHasNonPayingContractsUseCase: Provider - @Inject private lateinit var hedvigBuildConstants: HedvigBuildConstants @@ -206,7 +202,6 @@ class MainActivity : AppCompatActivity() { deepLinkChannel = deepLinkChannel, windowSizeClass = windowSizeClass, settingsDataStore = settingsDataStore, - getOnlyHasNonPayingContractsUseCase = getOnlyHasNonPayingContractsUseCase, featureManager = featureManager, splashIsRemovedSignal = splashIsRemovedSignal, authTokenService = authTokenService, diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt index a56f514f3d..298c9014ac 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt @@ -60,7 +60,6 @@ import com.hedvig.android.core.appreview.WaitUntilAppReviewDialogShouldBeOpenedU import com.hedvig.android.core.buildconstants.HedvigBuildConstants import com.hedvig.android.core.demomode.DemoManager import com.hedvig.android.core.demomode.Provider -import com.hedvig.android.data.paying.member.GetOnlyHasNonPayingContractsUseCase import com.hedvig.android.data.settings.datastore.SettingsDataStore import com.hedvig.android.design.system.hedvig.DemoModeLabel import com.hedvig.android.design.system.hedvig.Surface @@ -95,7 +94,6 @@ internal fun HedvigApp( deepLinkChannel: Channel, windowSizeClass: WindowSizeClass, settingsDataStore: SettingsDataStore, - getOnlyHasNonPayingContractsUseCase: Provider, featureManager: FeatureManager, splashIsRemovedSignal: Channel, authTokenService: AuthTokenService, @@ -118,7 +116,6 @@ internal fun HedvigApp( backstackController = backstackController, windowSizeClass = windowSizeClass, settingsDataStore = settingsDataStore, - getOnlyHasNonPayingContractsUseCase = getOnlyHasNonPayingContractsUseCase, featureManager = featureManager, missedPaymentNotificationServiceProvider = missedPaymentNotificationServiceProvider, ) diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigAppState.kt b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigAppState.kt index e2773d8a9f..7fe5ee0dac 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigAppState.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigAppState.kt @@ -12,7 +12,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import com.hedvig.android.app.navigation.BackstackController import com.hedvig.android.core.demomode.Provider -import com.hedvig.android.data.paying.member.GetOnlyHasNonPayingContractsUseCase import com.hedvig.android.data.settings.datastore.SettingsDataStore import com.hedvig.android.featureflags.FeatureManager import com.hedvig.android.featureflags.flags.Feature @@ -22,6 +21,7 @@ import com.hedvig.android.navigation.compose.NavigationSuiteType import com.hedvig.android.notification.badge.data.payment.MissedPaymentNotificationService import com.hedvig.android.theme.Theme import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.emitAll @@ -33,7 +33,6 @@ internal fun rememberHedvigAppState( backstackController: BackstackController, windowSizeClass: WindowSizeClass, settingsDataStore: SettingsDataStore, - getOnlyHasNonPayingContractsUseCase: Provider, featureManager: FeatureManager, missedPaymentNotificationServiceProvider: Provider, coroutineScope: CoroutineScope = rememberCoroutineScope(), @@ -43,7 +42,6 @@ internal fun rememberHedvigAppState( windowSizeClass, coroutineScope, settingsDataStore, - getOnlyHasNonPayingContractsUseCase, featureManager, missedPaymentNotificationServiceProvider, ) { @@ -52,7 +50,6 @@ internal fun rememberHedvigAppState( windowSizeClass = windowSizeClass, coroutineScope = coroutineScope, settingsDataStore = settingsDataStore, - getOnlyHasNonPayingContractsUseCase = getOnlyHasNonPayingContractsUseCase, featureManager = featureManager, missedPaymentNotificationServiceProvider = missedPaymentNotificationServiceProvider, ) @@ -66,7 +63,6 @@ internal class HedvigAppState( val windowSizeClass: WindowSizeClass, coroutineScope: CoroutineScope, private val settingsDataStore: SettingsDataStore, - getOnlyHasNonPayingContractsUseCase: Provider, featureManager: FeatureManager, missedPaymentNotificationServiceProvider: Provider, ) { @@ -94,25 +90,11 @@ internal class HedvigAppState( val isInScreenEligibleForCrossSells: Boolean get() = backstackController.currentDestination is CrossSellEligibleDestination - val topLevelTabs: StateFlow> = flow { - val onlyHasNonPayingContracts = getOnlyHasNonPayingContractsUseCase.provide().invoke().getOrNull() - emit( - buildList { - add(TopLevelTab.Home) - add(TopLevelTab.Insurances) - if (onlyHasNonPayingContracts != true) { - add(TopLevelTab.Forever) - } - add(TopLevelTab.Payments) - add(TopLevelTab.Profile) - }.toSet(), - ) - }.stateIn( - coroutineScope, - SharingStarted.Eagerly, + val topLevelTabs: StateFlow> = MutableStateFlow( setOf( TopLevelTab.Home, TopLevelTab.Insurances, + TopLevelTab.Forever, TopLevelTab.Payments, TopLevelTab.Profile, ), diff --git a/app/data/data-contract/src/commonMain/kotlin/com/hedvig/android/data/contract/ContractType.kt b/app/data/data-contract/src/commonMain/kotlin/com/hedvig/android/data/contract/ContractType.kt index 7124e8227e..018d0c7bed 100644 --- a/app/data/data-contract/src/commonMain/kotlin/com/hedvig/android/data/contract/ContractType.kt +++ b/app/data/data-contract/src/commonMain/kotlin/com/hedvig/android/data/contract/ContractType.kt @@ -65,6 +65,8 @@ enum class ContractType { SE_GROUP_APARTMENT_RENT, SE_QASA_LONG_TERM_RENTAL, SE_QASA_SHORT_TERM_RENTAL, + + SE_QASA_LANDLORD, NO_HOME_CONTENT_YOUTH_RENT, NO_HOME_CONTENT_YOUTH_OWN, DK_HOME_CONTENT_STUDENT_OWN, @@ -132,6 +134,7 @@ fun ContractType.isTrialContract() = when (this) { SE_DOG_PREMIUM, SE_DOG_STANDARD, UNKNOWN, + ContractType.SE_QASA_LANDLORD -> false SE_CAR_TRIAL_FULL, @@ -162,6 +165,7 @@ fun String.toContractType(): ContractType = when (this) { "SE_GROUP_APARTMENT_RENT" -> SE_GROUP_APARTMENT_RENT "SE_QASA_LONG_TERM_RENTAL" -> SE_QASA_LONG_TERM_RENTAL "SE_QASA_SHORT_TERM_RENTAL" -> SE_QASA_SHORT_TERM_RENTAL + "SE_QASA_LANDLORD" -> ContractType.SE_QASA_LANDLORD "NO_HOME_CONTENT_YOUTH_RENT" -> NO_HOME_CONTENT_YOUTH_RENT "NO_HOME_CONTENT_YOUTH_OWN" -> NO_HOME_CONTENT_YOUTH_OWN "DK_HOME_CONTENT_STUDENT_OWN" -> DK_HOME_CONTENT_STUDENT_OWN diff --git a/app/data/data-paying-member/src/main/graphql/ActiveInsuranceContractTypes.graphql b/app/data/data-paying-member/src/main/graphql/ActiveInsuranceContractTypes.graphql index f30d0179bb..b2b96de5d9 100644 --- a/app/data/data-paying-member/src/main/graphql/ActiveInsuranceContractTypes.graphql +++ b/app/data/data-paying-member/src/main/graphql/ActiveInsuranceContractTypes.graphql @@ -7,5 +7,12 @@ query ActiveInsuranceContractTypes { } } } + terminatedContracts { + currentAgreement { + productVariant { + typeOfContract + } + } + } } } diff --git a/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetChatRepositoryProvider.kt b/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetChatRepositoryProvider.kt deleted file mode 100644 index 91dce5a1aa..0000000000 --- a/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetChatRepositoryProvider.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.hedvig.android.data.paying.member - -import com.hedvig.android.core.common.di.AppScope -import com.hedvig.android.core.demomode.DemoManager -import com.hedvig.android.core.demomode.ProdOrDemoProvider -import com.hedvig.android.core.demomode.Provider -import dev.zacsweers.metro.ContributesBinding -import dev.zacsweers.metro.Inject -import dev.zacsweers.metro.SingleIn -import dev.zacsweers.metro.binding - -@Inject -@SingleIn(AppScope::class) -@ContributesBinding(AppScope::class, binding>()) -internal class GetOnlyHasNonPayingContractsUseCaseProvider( - override val demoManager: DemoManager, - override val demoImpl: GetOnlyHasNonPayingContractsUseCaseDemo, - override val prodImpl: GetOnlyHasNonPayingContractsUseCaseImpl, -) : ProdOrDemoProvider diff --git a/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetMemberTypeUseCase.kt b/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetMemberTypeUseCase.kt new file mode 100644 index 0000000000..a484dc06a5 --- /dev/null +++ b/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetMemberTypeUseCase.kt @@ -0,0 +1,92 @@ +package com.hedvig.android.data.paying.member + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.right +import com.apollographql.apollo.ApolloClient +import com.hedvig.android.apollo.ErrorMessage +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.core.common.di.AppScope +import com.hedvig.android.core.demomode.DemoManager +import com.hedvig.android.core.demomode.ProdOrDemoProvider +import com.hedvig.android.core.demomode.Provider +import com.hedvig.android.data.contract.ContractType +import com.hedvig.android.data.contract.toContractType +import com.hedvig.android.logger.logcat +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import dev.zacsweers.metro.binding +import octopus.ActiveInsuranceContractTypesQuery + +interface GetMemberTypeUseCase { + suspend fun invoke(): Either +} + +enum class MemberType { + QASA_ONLY_MEMBER, + + // in Payments: hide discounts, payment history, member payment details, + // in Payments: always show payout account; + // in Payments: show connect payout reminder if missing + // HelpCenter: hide Payments section + STANDARD_MEMBER, + + // current logic + STANDARD_TO_QASA_MEMBER + // in Payments: if no upcoming payments: hide discounts and member payment details + // in Payments: if upcoming payment and missing payin: show connect payin reminder, + // else show connect payout reminder if missing + // in Payments: always show payout account; +} + +@Inject +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class, binding>()) +internal class GetMemberTypeUseCaseProvider( + override val demoManager: DemoManager, + override val demoImpl: GetMemberTypeUseCaseDemo, + override val prodImpl: GetMemberTypeUseCaseImpl, +) : ProdOrDemoProvider + +@SingleIn(AppScope::class) +@Inject +internal class GetMemberTypeUseCaseImpl( + private val apolloClient: ApolloClient, +) : GetMemberTypeUseCase { + override suspend fun invoke(): Either { + return either { + val result = apolloClient.query(ActiveInsuranceContractTypesQuery()) + .safeExecute(::ErrorMessage) + .bind() + val activeContractsTypes = result.currentMember + .activeContracts + .map { it.currentAgreement.productVariant.typeOfContract.toContractType() } + val terminatedContractsTypes = result.currentMember + .terminatedContracts + .map { it.currentAgreement.productVariant.typeOfContract.toContractType() } + + val onlyQasaContracts = (activeContractsTypes.isNotEmpty() + || terminatedContractsTypes.isNotEmpty()) && + activeContractsTypes.all { it == ContractType.SE_QASA_LANDLORD } && + terminatedContractsTypes.all { it == ContractType.SE_QASA_LANDLORD } + if (onlyQasaContracts) return@either MemberType.QASA_ONLY_MEMBER + + val standardToQasa = activeContractsTypes.isNotEmpty() && + activeContractsTypes.all { it == ContractType.SE_QASA_LANDLORD } && + terminatedContractsTypes.any { it != ContractType.SE_QASA_LANDLORD } + if (standardToQasa) return@either MemberType.STANDARD_TO_QASA_MEMBER + + MemberType.STANDARD_MEMBER + } + } +} + +@SingleIn(AppScope::class) +@Inject +internal class GetMemberTypeUseCaseDemo : GetMemberTypeUseCase { + override suspend fun invoke(): Either { + return MemberType.STANDARD_MEMBER.right() + } +} diff --git a/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetOnlyHasNonPayingContractsUseCase.kt b/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetOnlyHasNonPayingContractsUseCase.kt deleted file mode 100644 index 6ec41dd2ac..0000000000 --- a/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetOnlyHasNonPayingContractsUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.hedvig.android.data.paying.member - -import arrow.core.Either -import com.hedvig.android.core.common.ErrorMessage - -/** - * Returns true when all the member's active contracts are non-paying contracts. - */ -interface GetOnlyHasNonPayingContractsUseCase { - suspend fun invoke(): Either -} diff --git a/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetOnlyHasNonPayingContractsUseCaseDemo.kt b/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetOnlyHasNonPayingContractsUseCaseDemo.kt deleted file mode 100644 index bb73bb5ad8..0000000000 --- a/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetOnlyHasNonPayingContractsUseCaseDemo.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.hedvig.android.data.paying.member - -import arrow.core.Either -import arrow.core.right -import com.hedvig.android.core.common.ErrorMessage -import dev.zacsweers.metro.Inject - -@Inject -internal class GetOnlyHasNonPayingContractsUseCaseDemo : GetOnlyHasNonPayingContractsUseCase { - override suspend fun invoke(): Either { - return false.right() - } -} diff --git a/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetOnlyHasNonPayingContractsUseCaseImpl.kt b/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetOnlyHasNonPayingContractsUseCaseImpl.kt deleted file mode 100644 index 9278462fe6..0000000000 --- a/app/data/data-paying-member/src/main/kotlin/com/hedvig/android/data/paying/member/GetOnlyHasNonPayingContractsUseCaseImpl.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.hedvig.android.data.paying.member - -import arrow.core.Either -import arrow.core.raise.either -import com.apollographql.apollo.ApolloClient -import com.hedvig.android.apollo.ErrorMessage -import com.hedvig.android.apollo.safeExecute -import com.hedvig.android.core.common.ErrorMessage -import com.hedvig.android.data.contract.ContractType -import com.hedvig.android.data.contract.toContractType -import dev.zacsweers.metro.Inject -import octopus.ActiveInsuranceContractTypesQuery - -@Inject -internal class GetOnlyHasNonPayingContractsUseCaseImpl( - private val apolloClient: ApolloClient, -) : GetOnlyHasNonPayingContractsUseCase { - override suspend fun invoke(): Either { - return either { - val contractTypes: List = - apolloClient.query(ActiveInsuranceContractTypesQuery()) - .safeExecute(::ErrorMessage) - .bind() - .currentMember - .activeContracts - .map { it.currentAgreement.productVariant.typeOfContract.toContractType() } - - contractTypes.all { it.isNonPayingContractType() } - } - } -} - -private fun ContractType.isNonPayingContractType(): Boolean { - return this == ContractType.SE_QASA_SHORT_TERM_RENTAL || this == ContractType.SE_QASA_LONG_TERM_RENTAL -} diff --git a/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql b/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql index dbf85e27b0..14e8d0d94e 100644 --- a/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql +++ b/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql @@ -24,6 +24,11 @@ fragment ClaimIntentStepContentFragment on ClaimIntentStepContent { ...FileUploadFragment ...SummaryFragment ...DeflectionFragment + ...DeflectionMessageFragment +} + +fragment DeflectionMessageFragment on ClaimIntentStepContentDeflectionMessage { + message } fragment FormFragment on ClaimIntentStepContentForm { diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt index 38ace0dcbe..f870fb032b 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt @@ -1122,6 +1122,7 @@ private fun ClaimIntentStep.clearContent(): ClaimIntentStep = when (val content is StepContent.Summary, is StepContent.Task, is StepContent.Deflect, + is StepContent.DeflectMessage, StepContent.Unknown, -> this } diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt index b443d59be8..6df1fd1eed 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt @@ -168,6 +168,14 @@ internal sealed interface StepContent { override val isSkippable: Boolean = false } + @Serializable + data class DeflectMessage( + @Contextual + val message: String, + ) : StepContent { + override val isSkippable: Boolean = false + } + object Unknown : StepContent { override val isSkippable: Boolean = false } diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt index 81b697e6ae..191359eb00 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt @@ -15,6 +15,7 @@ import octopus.fragment.ClaimIntentStepContentFragment import octopus.fragment.ContentSelectFragment import octopus.fragment.DeflectionFragment import octopus.fragment.DeflectionInfoBlockFragment +import octopus.fragment.DeflectionMessageFragment import octopus.fragment.FileUploadFragment import octopus.fragment.FormFragment import octopus.fragment.SummaryFragment @@ -181,6 +182,10 @@ private fun ClaimIntentStepContentFragment.toStepContent(locale: CommonLocale): ) } + is DeflectionMessageFragment -> StepContent.DeflectMessage( + message = message + ) + else -> { logcat { "ClaimIntentStepContentFragment: Unknown step" } raise(ClaimChatErrorMessage.NeedsUpdate) diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt index adfaeeadaa..449d2f0b2f 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt @@ -56,14 +56,17 @@ import androidx.compose.ui.semantics.traversalIndex import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.dropUnlessResumed import androidx.navigationevent.NavigationEventInfo import androidx.navigationevent.compose.NavigationEventHandler import androidx.navigationevent.compose.rememberNavigationEventState import coil3.ImageLoader import com.hedvig.android.compose.ui.plus import com.hedvig.android.core.uidata.UiFile +import com.hedvig.android.design.system.hedvig.ButtonDefaults import com.hedvig.android.design.system.hedvig.ErrorDialog import com.hedvig.android.design.system.hedvig.HedvigAlertDialog +import com.hedvig.android.design.system.hedvig.HedvigButton import com.hedvig.android.design.system.hedvig.HedvigErrorSection import com.hedvig.android.design.system.hedvig.HedvigFullScreenCenterAlignedProgress import com.hedvig.android.design.system.hedvig.HedvigText @@ -80,6 +83,8 @@ import com.hedvig.android.design.system.hedvig.icon.HedvigIcons import com.hedvig.android.logger.LogPriority import com.hedvig.android.logger.logcat import com.hedvig.feature.claim.chat.ClaimChatEvent +import com.hedvig.feature.claim.chat.ClaimChatEvent.* +import com.hedvig.feature.claim.chat.ClaimChatEvent.AudioRecording.* import com.hedvig.feature.claim.chat.ClaimChatUiState import com.hedvig.feature.claim.chat.ClaimChatViewModel import com.hedvig.feature.claim.chat.data.ClaimChatErrorMessage @@ -143,11 +148,11 @@ internal fun ClaimChatDestination( openAppSettings = openAppSettings, onNavigateToImageViewer = onNavigateToImageViewer, navigateToClaimOutcome = { - claimChatViewModel.emit(ClaimChatEvent.HandledOutcomeNavigation) + claimChatViewModel.emit(HandledOutcomeNavigation) navigateToClaimOutcome(it) }, navigateToDeflect = { stepId, deflect -> - claimChatViewModel.emit(ClaimChatEvent.HandledDeflectNavigation(stepId)) + claimChatViewModel.emit(HandledDeflectNavigation(stepId)) navigateToDeflect(deflect) }, appPackageId = appPackageId, @@ -177,7 +182,7 @@ internal fun ClaimChatScreenContent( when (uiState) { ClaimChatUiState.FailedToStart -> { HedvigErrorSection( - { claimChatViewModel.emit(ClaimChatEvent.RetryInitializing) }, + { claimChatViewModel.emit(RetryInitializing) }, ) } @@ -227,11 +232,11 @@ private fun ClaimChatScreen( freeTextHint = stringResource(Res.string.CLAIMS_TEXT_INPUT_POPOVER_PLACEHOLDER), freeTextTitle = stringResource(Res.string.CLAIMS_TEXT_INPUT_PLACEHOLDER), freeTextOnCancelClick = { - onEvent(ClaimChatEvent.CloseFreeChatOverlay) + onEvent(CloseFreeChatOverlay) }, freeTextOnSaveClick = { feedback -> - onEvent(ClaimChatEvent.UpdateFreeText(feedback)) - onEvent(ClaimChatEvent.CloseFreeChatOverlay) + onEvent(UpdateFreeText(feedback)) + onEvent(CloseFreeChatOverlay) }, shouldShowOverlay = uiState.showFreeTextOverlay != null, overlaidContent = { @@ -283,7 +288,7 @@ private fun ClaimChatScreenContent( title = stringResource(Res.string.general_error), message = stringResource(messageRes), onDismiss = { - onEvent(ClaimChatEvent.DismissErrorDialog) + onEvent(DismissErrorDialog) }, buttonText = when (uiState.errorSubmittingStep) { ClaimChatErrorMessage.NeedsUpdate -> stringResource(Res.string.EMBARK_UPDATE_APP_BUTTON) @@ -302,10 +307,10 @@ private fun ClaimChatScreenContent( confirmButtonLabel = stringResource(Res.string.CLAIM_CHAT_EDIT_ANSWER_BUTTON), dismissButtonLabel = stringResource(Res.string.general_cancel_button), onDismissRequest = { - onEvent(ClaimChatEvent.DismissConfirmEditDialog) + onEvent(DismissConfirmEditDialog) }, onConfirmClick = { - onEvent(ClaimChatEvent.Regret(uiState.showConfirmEditDialogForStep)) + onEvent(Regret(uiState.showConfirmEditDialogForStep)) }, ) } @@ -392,6 +397,13 @@ private fun ClaimChatScreenContent( imageLoader = imageLoader, openAppSettings = openAppSettings, modifier = Modifier.fillMaxSize(), + closeFlow = { + if (uiState.steps.size > 1) { + showCloseFlowDialog = true + } else { + navigateUp() + } + } ) } if (showScrollArrow) { @@ -430,6 +442,7 @@ private fun ClaimChatScrollableContent( appPackageId: String, imageLoader: ImageLoader, openAppSettings: () -> Unit, + closeFlow: () -> Unit, modifier: Modifier = Modifier, ) { val density = LocalDensity.current @@ -491,6 +504,7 @@ private fun ClaimChatScrollableContent( } else { Modifier }, + closeFlow = closeFlow ) } } @@ -540,6 +554,7 @@ private fun StepContentSection( appPackageId: String, imageLoader: ImageLoader, openAppSettings: () -> Unit, + closeFlow: () -> Unit, onResponseHeightChanged: (IntSize) -> Unit, modifier: Modifier = Modifier, ) { @@ -569,7 +584,7 @@ private fun StepContentSection( if (showBottomContent && isAnimationInProcess) { delay(bottomContentAnimationDuration.toLong()) isAnimationInProcess = false - onEvent(ClaimChatEvent.AddToShownAnimations(stepItem.id)) + onEvent(AddToShownAnimations(stepItem.id)) } } @@ -584,7 +599,7 @@ private fun StepContentSection( isAnimationComplete = !isAnimationInProcess, onAnimationFinished = { showBottomContent = true - onEvent(ClaimChatEvent.FinishTaskAnimation) + onEvent(FinishTaskAnimation) }, onNavigateToImageViewer = onNavigateToImageViewer, imageLoader = imageLoader, @@ -617,6 +632,7 @@ private fun StepContentSection( modifier = Modifier.onSizeChanged { size -> onResponseHeightChanged(size) }, + closeFlow = closeFlow, ) } } @@ -750,6 +766,7 @@ private fun StepBottomContent( appPackageId: String, imageLoader: ImageLoader, openAppSettings: () -> Unit, + closeFlow: ()-> Unit, modifier: Modifier = Modifier, ) { Column(modifier) { @@ -759,31 +776,31 @@ private fun StepBottomContent( item = stepItem, stepContent = stepItem.stepContent, onShowFreeText = { - onEvent(ClaimChatEvent.AudioRecording.SwitchToFreeText(stepItem.id)) + onEvent(SwitchToFreeText(stepItem.id)) }, onSwitchToAudioRecording = { - onEvent(ClaimChatEvent.AudioRecording.SwitchToAudioRecording(stepItem.id)) + onEvent(SwitchToAudioRecording(stepItem.id)) }, onLaunchFullScreenEditText = { restrictions -> - onEvent(ClaimChatEvent.OpenFreeTextOverlay(restrictions)) + onEvent(OpenFreeTextOverlay(restrictions)) }, startRecording = { - onEvent(ClaimChatEvent.AudioRecording.StartRecording(stepItem.id)) + onEvent(StartRecording(stepItem.id)) }, stopRecording = { - onEvent(ClaimChatEvent.AudioRecording.StopRecording(stepItem.id)) + onEvent(StopRecording(stepItem.id)) }, redoRecording = { - onEvent(ClaimChatEvent.AudioRecording.RedoRecording(stepItem.id)) + onEvent(RedoRecording(stepItem.id)) }, submitFreeText = { - onEvent(ClaimChatEvent.AudioRecording.SubmitTextInput(stepItem.id)) + onEvent(SubmitTextInput(stepItem.id)) }, submitAudioFile = { - onEvent(ClaimChatEvent.AudioRecording.SubmitAudioFile(stepItem.id)) + onEvent(SubmitAudioFile(stepItem.id)) }, onSkip = { - onEvent(ClaimChatEvent.Skip(stepItem.id)) + onEvent(Skip(stepItem.id)) }, isCurrentStep = isCurrentStep, clock = Clock.System, @@ -805,7 +822,7 @@ private fun StepBottomContent( currentContinueButtonLoading = currentContinueButtonLoading, canSkip = stepItem.stepContent.isSkippable, onSkip = { - onEvent(ClaimChatEvent.Skip(stepItem.id)) + onEvent(Skip(stepItem.id)) }, skipButtonLoading = currentSkipButtonLoading, stepContent = stepItem.stepContent, @@ -847,7 +864,7 @@ private fun StepBottomContent( is StepContent.Summary -> { ChatClaimSummaryBottomContent( onSubmit = { - onEvent(ClaimChatEvent.SubmitClaim(stepItem.id)) + onEvent(SubmitClaim(stepItem.id)) }, isCurrentStep = isCurrentStep, continueButtonLoading = currentContinueButtonLoading, @@ -868,7 +885,7 @@ private fun StepBottomContent( TaskStepBottomContent( stepItem.stepContent, onRetrySubmittingTask = { - onEvent(ClaimChatEvent.RetrySubmittingTaskStep(stepItem.id)) + onEvent(RetrySubmittingTaskStep(stepItem.id)) }, modifier = Modifier.fillMaxWidth(), ) @@ -879,6 +896,18 @@ private fun StepBottomContent( logcat(LogPriority.ERROR) { "StepContent.Unknown received in StepBottomContent" } } } + + is StepContent.DeflectMessage -> { + HedvigButton( + modifier = modifier.fillMaxWidth(), + text = stringResource(Res.string.general_close_button), + onClick = dropUnlessResumed { + closeFlow() + }, + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Secondary + ) + } } } } diff --git a/app/feature/feature-insurances/src/main/graphql/QueryInsuranceContracts.graphql b/app/feature/feature-insurances/src/main/graphql/QueryInsuranceContracts.graphql index 73ecdf2250..fd373ace75 100644 --- a/app/feature/feature-insurances/src/main/graphql/QueryInsuranceContracts.graphql +++ b/app/feature/feature-insurances/src/main/graphql/QueryInsuranceContracts.graphql @@ -36,7 +36,7 @@ query InsuranceContracts( ...MoneyFragment } } - cost { + itemCost { ...MonthlyCostFragment } basePremium { @@ -106,7 +106,7 @@ fragment AgreementFragment on Agreement { ...MoneyFragment } } - cost { + itemCost { ...MonthlyCostFragment } basePremium { 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 60df0cef75..47066cc6ef 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 @@ -144,7 +144,7 @@ private fun InsuranceContractsQuery.Data.CurrentMember.PendingContract.toPending premium = UiMoney.fromMoneyFragment(it.premium), ) }, - cost = this.cost.toMonthlyCost(), + cost = this.itemCost?.toMonthlyCost(), basePremium = UiMoney.fromMoneyFragment(this.basePremium), chipId = ChipIdState.NotRequired, supportsTermination = supportsTermination, @@ -185,7 +185,7 @@ private fun ContractFragment.toContract( premium = UiMoney.fromMoneyFragment(it.premium), ) }, - cost = currentAgreement.cost.toMonthlyCost(), + cost = currentAgreement.itemCost?.toMonthlyCost(), basePremium = UiMoney.fromMoneyFragment(currentAgreement.basePremium), ), upcomingInsuranceAgreement = upcomingChangedAgreement?.let { @@ -204,7 +204,7 @@ private fun ContractFragment.toContract( premium = UiMoney.fromMoneyFragment(it.premium), ) }, - cost = it.cost.toMonthlyCost(), + cost = it.itemCost?.toMonthlyCost(), basePremium = UiMoney.fromMoneyFragment(it.basePremium), ) }, 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 b7a2aa220c..561d19dd98 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 @@ -34,7 +34,7 @@ sealed interface InsuranceContract { val existingAddons: List val availableAddons: List - val cost: MonthlyCost + val cost: MonthlyCost? val basePremium: UiMoney @@ -69,7 +69,7 @@ sealed interface InsuranceContract { override val coInsured: List = currentInsuranceAgreement.coInsured override val coOwners: List = currentInsuranceAgreement.coOwners override val addons: List? = currentInsuranceAgreement.addons - override val cost: MonthlyCost = currentInsuranceAgreement.cost + override val cost: MonthlyCost? = currentInsuranceAgreement.cost override val basePremium: UiMoney = currentInsuranceAgreement.basePremium override val supportsRemovingAddon: Boolean = existingAddons.any { it.isRemovable && it.status !is ContractAddon.Status.EndsAt @@ -86,7 +86,7 @@ sealed interface InsuranceContract { override val productVariant: ProductVariant, override val displayItems: List, override val addons: List?, - override val cost: MonthlyCost, + override val cost: MonthlyCost?, override val basePremium: UiMoney, override val chipId: ChipIdState, override val supportsTermination: Boolean, @@ -157,7 +157,7 @@ data class InsuranceAgreement( val coOwners: List, val creationCause: CreationCause, val addons: List?, - val cost: MonthlyCost, + val cost: MonthlyCost?, val basePremium: UiMoney, ) { data class CoInsured( 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 6a353e9066..6d6fff47a3 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 @@ -229,6 +229,7 @@ private fun ContractDetailScreen( is Success -> { val contract = state.insuranceContract + val cost = contract.cost val consumedWindowInsets = remember { MutableWindowInsets() } LazyColumn( contentPadding = WindowInsets @@ -286,30 +287,32 @@ private fun ContractDetailScreen( ) { pageIndex -> when (pageIndex) { 0 -> { - val priceInfoForBottomSheet = PriceInfoForBottomSheet( - displayItems = buildList { - add( - contract.displayName to stringResource( - Res.string.OFFER_COST_AND_PREMIUM_PERIOD_ABBREVIATION, - contract.basePremium.toString(), - ), - ) - contract.addons?.forEach { addon -> + val priceInfoForBottomSheet = if (cost!=null) { + PriceInfoForBottomSheet( + displayItems = buildList { add( - addon.addonVariant.displayName - to stringResource( + contract.displayName to stringResource( + Res.string.OFFER_COST_AND_PREMIUM_PERIOD_ABBREVIATION, + contract.basePremium.toString(), + ), + ) + contract.addons?.forEach { addon -> + add( + addon.addonVariant.displayName + to stringResource( Res.string.OFFER_COST_AND_PREMIUM_PERIOD_ABBREVIATION, addon.premium.toString(), ), - ) - } - contract.cost.discounts.forEach { discount -> - add(discount.displayName to discount.displayValue) - } - }, - totalGross = contract.cost.monthlyGross, - totalNet = contract.cost.monthlyNet, - ) + ) + } + cost.discounts.forEach { discount -> + add(discount.displayName to discount.displayValue) + } + }, + totalGross = cost.monthlyGross, + totalNet = cost.monthlyNet, + ) + } else null YourInfoTab( contractId = contract.id, coverageItems = contract.displayItems, @@ -351,11 +354,13 @@ private fun ContractDetailScreen( isTerminated = contract.isTerminated, contractHolderDisplayName = contract.contractHolderDisplayName, contractHolderSSN = contract.contractHolderSSN, - priceToShow = contract.cost.monthlyNet, - showPriceInfoIcon = contract.cost.monthlyNet != contract.cost.monthlyGross || - contract.basePremium != contract.cost.monthlyNet, + priceToShow = cost?.monthlyNet, + showPriceInfoIcon = cost!=null && (cost.monthlyNet != cost.monthlyGross || + contract.basePremium != cost.monthlyNet), onInfoIconClick = { - costBreakdownBottomSheetState.show(priceInfoForBottomSheet) + if (priceInfoForBottomSheet!=null) { + costBreakdownBottomSheetState.show(priceInfoForBottomSheet) + } }, existingAddons = contract.existingAddons, availableAddons = contract.availableAddons, diff --git a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt index 3c7a8d2fc6..69a4124630 100644 --- a/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt +++ b/app/feature/feature-insurances/src/main/kotlin/com/hedvig/android/feature/insurances/insurancedetail/yourinfo/YourInfoTab.kt @@ -153,7 +153,7 @@ internal fun YourInfoTab( isTerminated: Boolean, contractHolderDisplayName: String, contractHolderSSN: String?, - priceToShow: UiMoney, + priceToShow: UiMoney?, showPriceInfoIcon: Boolean, onInfoIconClick: () -> Unit, existingAddons: List, @@ -199,7 +199,8 @@ internal fun YourInfoTab( } val upcomingChangesBottomSheet = rememberHedvigBottomSheetState() - if (upcomingChangesInsuranceAgreement != null) { + val upcomingChangesCost = upcomingChangesInsuranceAgreement?.cost + if (upcomingChangesInsuranceAgreement != null && upcomingChangesCost!=null) { val upcomingPriceInfoForBottomSheet = PriceInfoForBottomSheet( displayItems = buildList { add( @@ -299,12 +300,14 @@ internal fun YourInfoTab( ) { Column { CoverageRows(coverageItems, Modifier.padding(horizontal = 16.dp)) - PriceRow( - priceToShow, - showPriceInfoIcon, - onInfoIconClick, - Modifier.padding(horizontal = 16.dp), - ) + if (priceToShow!=null) { + PriceRow( + priceToShow, + showPriceInfoIcon, + onInfoIconClick, + Modifier.padding(horizontal = 16.dp), + ) + } if (allowEditCoInsured && coInsured.isNotEmpty()) { HorizontalDivider(Modifier.padding(horizontal = 16.dp)) Spacer(Modifier.height(16.dp)) diff --git a/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql b/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql index 81decbd041..337fe91c00 100644 --- a/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql +++ b/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql @@ -31,6 +31,12 @@ query UpcomingPayment { defaultPayinMethod { ...MemberPaymentMethodFragment } + payoutMethods { + ...MemberPaymentMethodFragment + } + defaultPayoutMethod { + ...MemberPaymentMethodFragment + } } } } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/PreviewData.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/PreviewData.kt index 9c5d7783df..44849f0661 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/PreviewData.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/PreviewData.kt @@ -2,6 +2,7 @@ package com.hedvig.android.feature.payments import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.paying.member.MemberType import com.hedvig.android.feature.payments.data.Discount import com.hedvig.android.feature.payments.data.ManualChargeToPrompt import com.hedvig.android.feature.payments.data.MemberCharge @@ -280,6 +281,7 @@ internal val paymentOverViewPreviewData: PaymentOverview ongoingCharges = listOf(OngoingCharge("id", LocalDate.fromEpochDays(401), UiMoney(200.0, UiCurrencyCode.SEK))), paymentConnection = PaymentConnection.Active, isManualChargeAllowed = ManualChargeToPrompt(UiMoney(200.0, UiCurrencyCode.SEK)), + memberType = MemberType.STANDARD_MEMBER ) } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetDiscountsOverviewUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetDiscountsOverviewUseCase.kt index 7718844f7f..e72e6b8c63 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetDiscountsOverviewUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetDiscountsOverviewUseCase.kt @@ -5,8 +5,6 @@ import arrow.core.raise.either import arrow.fx.coroutines.parZip import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.di.AppScope -import com.hedvig.android.core.demomode.Provider -import com.hedvig.android.data.paying.member.GetOnlyHasNonPayingContractsUseCase import com.hedvig.android.feature.payments.overview.data.ForeverInformation import com.hedvig.android.feature.payments.overview.data.GetForeverInformationUseCase import dev.zacsweers.metro.ContributesBinding @@ -23,22 +21,16 @@ internal interface GetDiscountsOverviewUseCase { internal class GetDiscountsOverviewUseCaseImpl( private val getDiscountsUseCase: GetDiscountsUseCase, private val getForeverInformationUseCase: GetForeverInformationUseCase, - private val getOnlyHasNonPayingContractsUseCaseProvider: Provider, ) : GetDiscountsOverviewUseCase { override suspend fun invoke(): Either { return either { parZip( { getForeverInformationUseCase.invoke().bind() }, { getDiscountsUseCase.invoke().bind() }, - { getOnlyHasNonPayingContractsUseCaseProvider.provide().invoke().bind() }, - ) { foreverInformation, discountedContracts, onlyHasNonPayingContracts -> + ) { foreverInformation, discountedContracts -> DiscountsOverview( discountedContracts = discountedContracts, - foreverInformation = if (onlyHasNonPayingContracts) { - null - } else { - foreverInformation - }, + foreverInformation = foreverInformation, ) } } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentConnection.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentConnection.kt index 0f9324240b..5646b106e1 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentConnection.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentConnection.kt @@ -7,9 +7,11 @@ sealed interface PaymentConnection { data object Pending : PaymentConnection - data class NeedsSetup( + data class NeedsPayinSetup( val terminationDateIfNotConnected: LocalDate?, ) : PaymentConnection + data object NeedsPayoutSetup: PaymentConnection + data object Unknown : PaymentConnection } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentOverview.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentOverview.kt index 06376e27c3..51369543ef 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentOverview.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentOverview.kt @@ -1,6 +1,7 @@ package com.hedvig.android.feature.payments.data import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.paying.member.MemberType import kotlinx.datetime.LocalDate data class PaymentOverview( @@ -8,6 +9,7 @@ data class PaymentOverview( val ongoingCharges: List, val paymentConnection: PaymentConnection, val isManualChargeAllowed: ManualChargeToPrompt?, + val memberType: MemberType ) { data class OngoingCharge( val id: String, diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt index 4820484050..609cf35087 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt @@ -9,15 +9,20 @@ import com.apollographql.apollo.cache.normalized.fetchPolicy import com.hedvig.android.apollo.ErrorMessage import com.hedvig.android.apollo.safeExecute import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.core.demomode.Provider import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.paying.member.GetMemberTypeUseCase +import com.hedvig.android.data.paying.member.MemberType import com.hedvig.android.feature.payments.data.ManualChargeToPrompt import com.hedvig.android.feature.payments.data.MemberCharge import com.hedvig.android.feature.payments.data.MemberChargeShortInfo import com.hedvig.android.feature.payments.data.PaymentConnection +import com.hedvig.android.feature.payments.data.PaymentConnection.* import com.hedvig.android.feature.payments.data.PaymentOverview import com.hedvig.android.feature.payments.data.PaymentOverview.OngoingCharge import com.hedvig.android.feature.payments.data.toFailedCharge +import com.hedvig.android.logger.logcat import dev.zacsweers.metro.Inject import kotlin.time.Clock import kotlin.time.Duration.Companion.days @@ -36,12 +41,14 @@ internal interface GetUpcomingPaymentUseCase { internal data class GetUpcomingPaymentUseCaseImpl( val apolloClient: ApolloClient, val clock: Clock, + val getMemberTypeUseCaseProvider: Provider, ) : GetUpcomingPaymentUseCase { override suspend fun invoke(): Either = either { val result = apolloClient.query(UpcomingPaymentQuery()) .fetchPolicy(FetchPolicy.NetworkFirst) .safeExecute(::ErrorMessage) .bind() + val memberType = getMemberTypeUseCaseProvider.provide().invoke().bind() val missedChargeIdToChargeManually: String? = result.currentMember.missedChargeIdToChargeManually @@ -71,6 +78,9 @@ internal data class GetUpcomingPaymentUseCaseImpl( val paymentMethods = result.currentMember.paymentMethods val payinMethod = paymentMethods.defaultPayinMethod ?: paymentMethods.payinMethods.find { it.isDefault } + logcat {"Mariia: payinMethod $payinMethod"} + val payoutMethod = paymentMethods.defaultPayoutMethod + ?: paymentMethods.payoutMethods.find { it.isDefault } if (payinMethod == null) { val firstKnownTerminationDateForContractTerminatedDueToMissedPayments = result .currentMember @@ -79,17 +89,37 @@ internal data class GetUpcomingPaymentUseCaseImpl( .mapNotNull { it.terminationDate } .sorted() .firstOrNull() - return@run PaymentConnection.NeedsSetup( - firstKnownTerminationDateForContractTerminatedDueToMissedPayments, - ) + + when (memberType) { + + MemberType.STANDARD_MEMBER -> return@run NeedsPayinSetup( + firstKnownTerminationDateForContractTerminatedDueToMissedPayments, + ) + + MemberType.QASA_ONLY_MEMBER -> { + if (payoutMethod == null) { + return@run PaymentConnection.NeedsPayoutSetup + } else return@run PaymentConnection.Active + } + + MemberType.STANDARD_TO_QASA_MEMBER -> TODO() + } } when (payinMethod.status) { - MemberPaymentMethodStatus.ACTIVE -> PaymentConnection.Active + MemberPaymentMethodStatus.ACTIVE -> { + logcat {"Mariia: MemberPaymentMethodStatus.ACTIVE"} + logcat {"Mariia: payoutMethod $payoutMethod"} + if (payoutMethod == null) { + return@run PaymentConnection.NeedsPayoutSetup + } else return@run PaymentConnection.Active + } + MemberPaymentMethodStatus.PENDING -> PaymentConnection.Pending MemberPaymentMethodStatus.UNKNOWN__ -> PaymentConnection.Unknown } }, isManualChargeAllowed = isManualChargeAllowed, + memberType = memberType ) } } @@ -124,6 +154,7 @@ internal class GetUpcomingPaymentUseCaseDemo( emptyList(), PaymentConnection.Unknown, isManualChargeAllowed = null, + memberType = MemberType.STANDARD_MEMBER ).right() } } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt index 0c4091d20d..9437a42547 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt @@ -46,6 +46,7 @@ import com.hedvig.android.core.common.safeCast import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiCurrencyCode.SEK import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.paying.member.MemberType import com.hedvig.android.design.system.hedvig.ButtonDefaults import com.hedvig.android.design.system.hedvig.HedvigButton import com.hedvig.android.design.system.hedvig.HedvigCard @@ -58,11 +59,12 @@ import com.hedvig.android.design.system.hedvig.HedvigTheme import com.hedvig.android.design.system.hedvig.HorizontalDivider import com.hedvig.android.design.system.hedvig.HorizontalItemsWithMaximumSpaceTaken import com.hedvig.android.design.system.hedvig.Icon -import com.hedvig.android.design.system.hedvig.NotificationDefaults +import com.hedvig.android.design.system.hedvig.NotificationDefaults.InfoCardStyle import com.hedvig.android.design.system.hedvig.NotificationDefaults.InfoCardStyle.Button import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority.Info import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.debugBorder import com.hedvig.android.design.system.hedvig.hedvigDropShadow import com.hedvig.android.design.system.hedvig.icon.Campaign import com.hedvig.android.design.system.hedvig.icon.Card @@ -106,6 +108,8 @@ import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_BUTTON import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_TITLE import hedvig.resources.PAYMENTS_PROCESSING_PAYMENT import hedvig.resources.PAYMENTS_UPCOMING_PAYMENT +import hedvig.resources.PAYOUT_ADD_PAYOUT_METHOD +import hedvig.resources.PAYOUT_MISSING_INFO import hedvig.resources.PAYOUT_PAGE_HEADING import hedvig.resources.PROFILE_PAYMENT_CONNECT_DIRECT_DEBIT_TITLE import hedvig.resources.R @@ -276,7 +280,7 @@ private fun PaymentsContent( } val upcomingPayment = (uiState as? Content)?.upcomingPayment if (upcomingPayment == NoUpcomingPayment) { - if (ongoingCharges.isNullOrEmpty()) { + if (ongoingCharges.isNullOrEmpty() && uiState.memberType != MemberType.QASA_ONLY_MEMBER) { HedvigInformationSection( stringResource(Res.string.PAYMENTS_NO_PAYMENTS_IN_PROGRESS), modifier = Modifier @@ -293,38 +297,14 @@ private fun PaymentsContent( .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)), ) } - UpcomingPaymentInfoCard( - upcomingPaymentInfo = (uiState as? Content)?.upcomingPaymentInfo, - modifier = Modifier - .padding(horizontal = 16.dp) - .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)), - ) - val showConnectedPaymentInfo = uiState is Content && - uiState.connectedPaymentInfo is ConnectedPaymentInfo.NeedsSetup - AnimatedVisibility( - visibleState = remember { MutableTransitionState(showConnectedPaymentInfo) }.apply { - targetState = showConnectedPaymentInfo - }, - enter = expandVertically(expandFrom = Alignment.CenterVertically), - ) { - CardNotConnectedWarningCard( - connectedPaymentInfo = (uiState as? Content)?.connectedPaymentInfo as? ConnectedPaymentInfo.NeedsSetup, - onChangeBankAccount = onChangeBankAccount, + if (uiState is Content) { + UpcomingPaymentInfoCard( + upcomingPaymentInfo = uiState.upcomingPaymentInfo, modifier = Modifier .padding(horizontal = 16.dp) .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)), ) - } - PaymentsListItems( - uiState, - onDiscountClicked = onDiscountClicked, - onPaymentHistoryClicked = onPaymentHistoryClicked, - onPayoutAccountClicked = onPayoutAccountClicked, - onPaymentDetailsClicked = onPaymentDetailsClicked, - showPayoutButton = (uiState as? Content)?.showPayoutButton == true, - ) - if (uiState is Content) { when (uiState.connectedPaymentInfo) { ConnectedPaymentInfo.Pending -> { HedvigNotificationCard( @@ -340,19 +320,51 @@ private fun PaymentsContent( ) } - is ConnectedPaymentInfo.NeedsSetup, + is ConnectedPaymentInfo.NeedsPayinSetup -> { + CardNotConnectedWarningCard( + connectedPaymentInfo = uiState.connectedPaymentInfo as? ConnectedPaymentInfo.NeedsPayinSetup, + onChangeBankAccount = onChangeBankAccount, + modifier = Modifier + .padding(horizontal = 16.dp) + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)), + ) + } + + ConnectedPaymentInfo.NeedsPayoutSetup -> { + HedvigNotificationCard( + message = stringResource(Res.string.PAYOUT_MISSING_INFO), + modifier = Modifier + .padding(horizontal = 16.dp) + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)), + priority = NotificationPriority.Attention, + style = Button( + buttonText = stringResource(Res.string.PAYOUT_ADD_PAYOUT_METHOD), + onButtonClick = onPayoutAccountClicked, + ), + minLines = 1, + ) + } + ConnectedPaymentInfo.Unknown, is ConnectedPaymentInfo.Active, - -> { - } + -> {} } } + + PaymentsListItems( + uiState, + onDiscountClicked = onDiscountClicked, + onPaymentHistoryClicked = onPaymentHistoryClicked, + onPayoutAccountClicked = onPayoutAccountClicked, + onPaymentDetailsClicked = onPaymentDetailsClicked, + showPayoutButton = (uiState as? Content)?.showPayoutButton == true, + ) } } @Composable private fun CardNotConnectedWarningCard( - connectedPaymentInfo: ConnectedPaymentInfo.NeedsSetup?, + connectedPaymentInfo: ConnectedPaymentInfo.NeedsPayinSetup?, onChangeBankAccount: () -> Unit, modifier: Modifier = Modifier, ) { @@ -379,9 +391,10 @@ private fun CardNotConnectedWarningCard( @Composable private fun UpcomingPaymentInfoCard(upcomingPaymentInfo: UpcomingPaymentInfo?, modifier: Modifier = Modifier) { + if (upcomingPaymentInfo == NoInfo || upcomingPaymentInfo == null) return Box(modifier) { when (upcomingPaymentInfo) { - NoInfo -> {} + NoInfo, null -> {} InProgress -> { HedvigNotificationCard( @@ -401,13 +414,11 @@ private fun UpcomingPaymentInfoCard(upcomingPaymentInfo: UpcomingPaymentInfo?, m monthDateFormatter.format(upcomingPaymentInfo.failedPaymentStartDate), monthDateFormatter.format(upcomingPaymentInfo.failedPaymentEndDate), ), - style = NotificationDefaults.InfoCardStyle.Default, + style = InfoCardStyle.Default, ) } } } - - null -> {} } } } @@ -425,41 +436,67 @@ private fun PaymentsListItems( .padding(horizontal = 16.dp) .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)) Column { - PaymentsListItem( - text = stringResource(Res.string.PAYMENTS_DISCOUNTS_SECTION_TITLE), - icon = { - Icon( - imageVector = HedvigIcons.Campaign, - contentDescription = null, - tint = HedvigTheme.colorScheme.signalGreenElement, - modifier = Modifier.size(24.dp), + if (uiState is Content) { + val showDiscountsItem = when (uiState.memberType) { + MemberType.QASA_ONLY_MEMBER, + MemberType.STANDARD_TO_QASA_MEMBER, + -> false + + MemberType.STANDARD_MEMBER -> true + } + if (showDiscountsItem) { + PaymentsListItem( + text = stringResource(Res.string.PAYMENTS_DISCOUNTS_SECTION_TITLE), + icon = { + Icon( + imageVector = HedvigIcons.Campaign, + contentDescription = null, + tint = HedvigTheme.colorScheme.signalGreenElement, + modifier = Modifier.size(24.dp), + ) + }, + modifier = Modifier + .clickable(onClick = onDiscountClicked) + .then(listItemsSideSpacingModifier) + .padding(vertical = 16.dp) + .fillMaxWidth(), ) - }, - modifier = Modifier - .clickable(onClick = onDiscountClicked) - .then(listItemsSideSpacingModifier) - .padding(vertical = 16.dp) - .fillMaxWidth(), - ) - HorizontalDivider(modifier = listItemsSideSpacingModifier) - PaymentsListItem( - text = stringResource(Res.string.PAYMENTS_PAYMENT_HISTORY_BUTTON_LABEL), - icon = { - Icon( - imageVector = HedvigIcons.Clock, - contentDescription = null, - modifier = Modifier.size(24.dp), + HorizontalDivider(modifier = listItemsSideSpacingModifier) + } + + + val showPaymentHistoryItem = when (uiState.memberType) { + MemberType.QASA_ONLY_MEMBER -> false + MemberType.STANDARD_MEMBER, + MemberType.STANDARD_TO_QASA_MEMBER, + -> true + } + if (showPaymentHistoryItem) { + PaymentsListItem( + text = stringResource(Res.string.PAYMENTS_PAYMENT_HISTORY_BUTTON_LABEL), + icon = { + Icon( + imageVector = HedvigIcons.Clock, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + }, + modifier = Modifier + .clickable(onClick = onPaymentHistoryClicked) + .then(listItemsSideSpacingModifier) + .padding(vertical = 16.dp) + .fillMaxWidth(), ) - }, - modifier = Modifier - .clickable(onClick = onPaymentHistoryClicked) - .then(listItemsSideSpacingModifier) - .padding(vertical = 16.dp) - .fillMaxWidth(), - ) - if (uiState is Content) { - if (uiState.connectedPaymentInfo is ConnectedPaymentInfo.Active) { HorizontalDivider(listItemsSideSpacingModifier) + } + + val showPaymentDetailsItem = when (uiState.memberType) { + MemberType.QASA_ONLY_MEMBER -> false + MemberType.STANDARD_MEMBER, + MemberType.STANDARD_TO_QASA_MEMBER, + -> uiState.connectedPaymentInfo is ConnectedPaymentInfo.Active + } + if (showPaymentDetailsItem) { PaymentsListItem( text = stringResource(Res.string.PAYMENTS_PAYMENT_DETAILS_INFO_TITLE), icon = { @@ -475,25 +512,34 @@ private fun PaymentsListItems( .padding(vertical = 16.dp) .fillMaxWidth(), ) + HorizontalDivider(modifier = listItemsSideSpacingModifier) + } + + val showPayoutItem = when (uiState.memberType) { + MemberType.QASA_ONLY_MEMBER, + MemberType.STANDARD_TO_QASA_MEMBER, + -> true + + MemberType.STANDARD_MEMBER -> showPayoutButton + } + if (showPayoutItem) { + PaymentsListItem( + text = stringResource(Res.string.PAYOUT_PAGE_HEADING), + icon = { + Icon( + imageVector = HedvigIcons.PaymentOutline, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + }, + modifier = Modifier + .clickable(onClick = onPayoutAccountClicked) + .then(listItemsSideSpacingModifier) + .padding(vertical = 16.dp) + .fillMaxWidth(), + ) + HorizontalDivider(modifier = listItemsSideSpacingModifier) } - } - if (showPayoutButton) { - HorizontalDivider(modifier = listItemsSideSpacingModifier) - PaymentsListItem( - text = stringResource(Res.string.PAYOUT_PAGE_HEADING), - icon = { - Icon( - imageVector = HedvigIcons.PaymentOutline, - contentDescription = null, - modifier = Modifier.size(24.dp), - ) - }, - modifier = Modifier - .clickable(onClick = onPayoutAccountClicked) - .then(listItemsSideSpacingModifier) - .padding(vertical = 16.dp) - .fillMaxWidth(), - ) } } } @@ -767,6 +813,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< ongoingCharges = listOf(OngoingCharge("id", LocalDate.fromEpochDays(401), UiMoney(200.0, SEK))), connectedPaymentInfo = ConnectedPaymentInfo.Active, showPayoutButton = true, + memberType = MemberType.STANDARD_MEMBER, ), ) add( @@ -781,6 +828,24 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.Active, showPayoutButton = false, + memberType = MemberType.STANDARD_MEMBER, + ), + ) + add( + Content( + isRetrying = false, + upcomingPayment = UpcomingPayment.Content( + UiMoney(100.0, SEK), + System.now().toLocalDateTime(TimeZone.UTC).date, + "rdg", + ), + upcomingPaymentInfo = NoInfo, + ongoingCharges = emptyList(), + connectedPaymentInfo = ConnectedPaymentInfo.NeedsPayinSetup( + null, + ), + showPayoutButton = false, + memberType = MemberType.STANDARD_TO_QASA_MEMBER, ), ) add( @@ -795,6 +860,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.Active, showPayoutButton = false, + memberType = MemberType.STANDARD_MEMBER, ), ) add( @@ -815,6 +881,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.Active, showPayoutButton = false, + memberType = MemberType.STANDARD_MEMBER, ), ) add( @@ -829,6 +896,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.Pending, showPayoutButton = false, + memberType = MemberType.STANDARD_MEMBER, ), ) add( @@ -845,10 +913,11 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< isManualChargeAllowed = null, ), ongoingCharges = emptyList(), - connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( + connectedPaymentInfo = ConnectedPaymentInfo.NeedsPayinSetup( null, ), showPayoutButton = false, + memberType = MemberType.STANDARD_MEMBER, ), ) add( @@ -861,10 +930,11 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< ), upcomingPaymentInfo = NoInfo, ongoingCharges = emptyList(), - connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( + connectedPaymentInfo = ConnectedPaymentInfo.NeedsPayinSetup( null, ), showPayoutButton = false, + memberType = MemberType.STANDARD_MEMBER, ), ) add( @@ -881,10 +951,11 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< isManualChargeAllowed = null, ), ongoingCharges = emptyList(), - connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( + connectedPaymentInfo = ConnectedPaymentInfo.NeedsPayinSetup( dueDateToConnect = System.now().plus(30.days).toLocalDateTime(TimeZone.UTC).date, ), showPayoutButton = false, + memberType = MemberType.STANDARD_MEMBER, ), ) add( @@ -901,10 +972,70 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< isManualChargeAllowed = null, ), ongoingCharges = emptyList(), - connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( + connectedPaymentInfo = ConnectedPaymentInfo.NeedsPayinSetup( System.now().plus(30.days).toLocalDateTime(TimeZone.UTC).date, ), showPayoutButton = false, + memberType = MemberType.STANDARD_MEMBER, + ), + ) + add( + Content( + isRetrying = false, + upcomingPayment = NoUpcomingPayment, + upcomingPaymentInfo = NoInfo, + ongoingCharges = emptyList(), + connectedPaymentInfo = ConnectedPaymentInfo.NeedsPayoutSetup, + showPayoutButton = false, + memberType = MemberType.STANDARD_TO_QASA_MEMBER, + ), + ) + add( + Content( + isRetrying = false, + upcomingPayment = NoUpcomingPayment, + upcomingPaymentInfo = NoInfo, + ongoingCharges = emptyList(), + connectedPaymentInfo = ConnectedPaymentInfo.Active, + showPayoutButton = false, + memberType = MemberType.STANDARD_TO_QASA_MEMBER, + ), + ) + add( + Content( + isRetrying = false, + upcomingPayment = NoUpcomingPayment, + upcomingPaymentInfo = NoInfo, + ongoingCharges = emptyList(), + connectedPaymentInfo = ConnectedPaymentInfo.NeedsPayoutSetup, + showPayoutButton = false, + memberType = MemberType.QASA_ONLY_MEMBER, + ), + ) + add( + Content( + isRetrying = false, + upcomingPayment = NoUpcomingPayment, + upcomingPaymentInfo = NoInfo, + ongoingCharges = emptyList(), + connectedPaymentInfo = ConnectedPaymentInfo.Active, + showPayoutButton = false, + memberType = MemberType.QASA_ONLY_MEMBER, + ), + ) + add( + Content( + isRetrying = false, + upcomingPayment = UpcomingPayment.Content( + UiMoney(100.0, SEK), + System.now().toLocalDateTime(TimeZone.UTC).date, + "w345423t6", + ), + upcomingPaymentInfo = UpcomingPaymentInfo.NoInfo, + ongoingCharges = emptyList(), + connectedPaymentInfo = ConnectedPaymentInfo.NeedsPayoutSetup, + showPayoutButton = true, + memberType = MemberType.STANDARD_MEMBER, ), ) }, diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt index c7a403a282..7b7e00797a 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt @@ -11,12 +11,12 @@ import arrow.core.Either import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.demomode.Provider import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.paying.member.MemberType import com.hedvig.android.feature.payments.data.ManualChargeToPrompt import com.hedvig.android.feature.payments.data.MemberCharge -import com.hedvig.android.feature.payments.data.MemberPaymentChargeMethod import com.hedvig.android.feature.payments.data.PaymentConnection import com.hedvig.android.feature.payments.data.PaymentConnection.Active -import com.hedvig.android.feature.payments.data.PaymentConnection.NeedsSetup +import com.hedvig.android.feature.payments.data.PaymentConnection.NeedsPayinSetup import com.hedvig.android.feature.payments.data.PaymentConnection.Pending import com.hedvig.android.feature.payments.data.PaymentConnection.Unknown import com.hedvig.android.feature.payments.data.PaymentOverview @@ -28,8 +28,6 @@ import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.firstOrNull import kotlinx.datetime.LocalDate internal class PaymentsPresenter( @@ -87,6 +85,7 @@ internal class PaymentsPresenter( ongoingCharges = paymentOverview.ongoingCharges, connectedPaymentInfo = paymentOverview.paymentConnection.toConnectedPaymentInfo(), showPayoutButton = shouldShowPayout, + memberType = paymentOverview.memberType, ) }, ) @@ -122,11 +121,12 @@ private fun PaymentConnection.toConnectedPaymentInfo(): ConnectedPaymentInfo { Pending -> ConnectedPaymentInfo.Pending - is NeedsSetup -> ConnectedPaymentInfo.NeedsSetup( + is NeedsPayinSetup -> ConnectedPaymentInfo.NeedsPayinSetup( dueDateToConnect = terminationDateIfNotConnected, ) Unknown -> ConnectedPaymentInfo.Unknown + PaymentConnection.NeedsPayoutSetup -> ConnectedPaymentInfo.NeedsPayoutSetup } } @@ -146,6 +146,7 @@ internal sealed interface PaymentsUiState { val ongoingCharges: List, val connectedPaymentInfo: ConnectedPaymentInfo, val showPayoutButton: Boolean, + val memberType: MemberType ) : PaymentsUiState { sealed interface UpcomingPayment { data object NoUpcomingPayment : UpcomingPayment @@ -172,13 +173,15 @@ internal sealed interface PaymentsUiState { sealed interface ConnectedPaymentInfo { object Unknown : ConnectedPaymentInfo - data class NeedsSetup( + data class NeedsPayinSetup( val dueDateToConnect: LocalDate?, ) : ConnectedPaymentInfo data object Pending : ConnectedPaymentInfo data object Active : ConnectedPaymentInfo + + data object NeedsPayoutSetup: ConnectedPaymentInfo } } } diff --git a/app/member-reminders/member-reminders-public/src/main/graphql/QueryGetPayinMethodStatus.graphql b/app/member-reminders/member-reminders-public/src/main/graphql/QueryGetPayinMethodStatus.graphql index 508833a63d..a812148af7 100644 --- a/app/member-reminders/member-reminders-public/src/main/graphql/QueryGetPayinMethodStatus.graphql +++ b/app/member-reminders/member-reminders-public/src/main/graphql/QueryGetPayinMethodStatus.graphql @@ -9,6 +9,7 @@ query GetPayinMethodStatus { id } paymentMethods { + missingConnection payoutMethods { status isDefault diff --git a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCase.kt b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCase.kt index c809022edd..2aebad6462 100644 --- a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCase.kt +++ b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCase.kt @@ -11,14 +11,13 @@ import com.hedvig.android.apollo.safeExecute import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.di.AppScope import com.hedvig.android.core.demomode.Provider -import com.hedvig.android.data.paying.member.GetOnlyHasNonPayingContractsUseCase import com.hedvig.android.logger.logcat import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn import kotlinx.datetime.LocalDate import octopus.GetPayinMethodStatusQuery -import octopus.type.MemberPaymentConnectionStatus +import octopus.type.MissingPaymentConnection internal interface GetConnectPaymentReminderUseCase { suspend fun invoke(): Either @@ -28,20 +27,15 @@ internal interface GetConnectPaymentReminderUseCase { @SingleIn(AppScope::class) @Inject internal class GetConnectPaymentReminderUseCaseImpl( - private val apolloClient: ApolloClient, - private val getOnlyHasNonPayingContractsUseCaseProvider: Provider, -) : GetConnectPaymentReminderUseCase { + private val apolloClient: ApolloClient) : GetConnectPaymentReminderUseCase { override suspend fun invoke(): Either { return either { - val onlyHasNonPayingContracts = getOnlyHasNonPayingContractsUseCaseProvider.provide().invoke().getOrNull() == true - ensure(onlyHasNonPayingContracts == false) { - ConnectPaymentReminderError.DomainError.NonPayingMember - } val result = apolloClient.query(GetPayinMethodStatusQuery()) .fetchPolicy(FetchPolicy.NetworkOnly) .safeExecute(::ErrorMessage) .mapLeft(ConnectPaymentReminderError::NetworkError) .bind() + val missingPaymentsContractTerminationDate = result.currentMember.activeContracts .filter { it.terminationDueToMissedPayments } .sortedBy { it.terminationDate } @@ -50,16 +44,14 @@ internal class GetConnectPaymentReminderUseCaseImpl( if (missingPaymentsContractTerminationDate != null) { return@either PaymentReminder.ShowMissingPaymentsReminder(missingPaymentsContractTerminationDate) } - val payStatus = result.currentMember.paymentInformation.status - if (payStatus == MemberPaymentConnectionStatus.NEEDS_SETUP) { - return@either PaymentReminder.ShowConnectPaymentReminder - } - val hasDefaultPayoutMethod = result.currentMember.paymentMethods.payoutMethods.any { it.isDefault } - val hasAvailableMethods = result.currentMember.paymentMethods.availableMethods.isNotEmpty() - if (!hasDefaultPayoutMethod && hasAvailableMethods) { - return@either PaymentReminder.ShowConnectPayoutReminder + + val missingConnection = result.currentMember.paymentMethods.missingConnection + when (missingConnection) { + MissingPaymentConnection.PAYIN -> return@either PaymentReminder.ShowConnectPaymentReminder + MissingPaymentConnection.PAYOUT -> return@either PaymentReminder.ShowConnectPayoutReminder + MissingPaymentConnection.UNKNOWN__ , null -> raise(ConnectPaymentReminderError.DomainError.AlreadySetup) } - raise(ConnectPaymentReminderError.DomainError.AlreadySetup) + }.onLeft { if (it !is ConnectPaymentReminderError.DomainError) { logcat { "GetConnectPaymentReminderUseCase failed with error:$it" } diff --git a/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCaseTest.kt b/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCaseTest.kt index 904b77e080..10568df5eb 100644 --- a/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCaseTest.kt +++ b/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCaseTest.kt @@ -1,7 +1,5 @@ package com.hedvig.android.memberreminders -import arrow.core.Either -import arrow.core.right import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf @@ -12,21 +10,19 @@ import com.apollographql.apollo.testing.enqueueTestResponse import com.google.testing.junit.testparameterinjector.TestParameterInjector import com.hedvig.android.apollo.octopus.test.OctopusFakeResolver import com.hedvig.android.apollo.test.TestApolloClientRule -import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.test.isLeft import com.hedvig.android.core.common.test.isRight -import com.hedvig.android.core.demomode.Provider -import com.hedvig.android.data.paying.member.GetOnlyHasNonPayingContractsUseCase import com.hedvig.android.logger.TestLogcatLoggingRule import com.hedvig.android.memberreminders.PaymentReminder.ShowConnectPaymentReminder +import com.hedvig.android.memberreminders.PaymentReminder.ShowConnectPayoutReminder import com.hedvig.android.memberreminders.PaymentReminder.ShowMissingPaymentsReminder import kotlinx.coroutines.test.runTest import kotlinx.datetime.LocalDate import octopus.GetPayinMethodStatusQuery -import octopus.type.MemberPaymentConnectionStatus +import octopus.type.MissingPaymentConnection import octopus.type.buildContract import octopus.type.buildMember -import octopus.type.buildMemberPaymentInformation +import octopus.type.buildMemberPaymentMethods import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -43,17 +39,14 @@ class GetConnectPaymentReminderUseCaseTest { get() = testApolloClientRule.apolloClient @Test - fun `when payin method needs setup, show the reminder`() = runTest { - val getConnectPaymentReminderUseCase = GetConnectPaymentReminderUseCaseImpl( - apolloClient, - doesHavePayingContractsUseCaseProvider, - ) + fun `when payin connection is missing, show the connect payment reminder`() = runTest { + val getConnectPaymentReminderUseCase = GetConnectPaymentReminderUseCaseImpl(apolloClient) apolloClient.enqueueTestResponse( GetPayinMethodStatusQuery(), GetPayinMethodStatusQuery.Data(OctopusFakeResolver) { currentMember = buildMember { - paymentInformation = buildMemberPaymentInformation { - status = MemberPaymentConnectionStatus.NEEDS_SETUP + paymentMethods = buildMemberPaymentMethods { + missingConnection = MissingPaymentConnection.PAYIN } } }, @@ -65,19 +58,32 @@ class GetConnectPaymentReminderUseCaseTest { } @Test - fun `when payin method does not need setup but is missing payments, show the missing payment reminder`() = runTest { - val getConnectPaymentReminderUseCase = GetConnectPaymentReminderUseCaseImpl( - apolloClient, - doesHavePayingContractsUseCaseProvider, + fun `when payout connection is missing, show the connect payout reminder`() = runTest { + val getConnectPaymentReminderUseCase = GetConnectPaymentReminderUseCaseImpl(apolloClient) + apolloClient.enqueueTestResponse( + GetPayinMethodStatusQuery(), + GetPayinMethodStatusQuery.Data(OctopusFakeResolver) { + currentMember = buildMember { + paymentMethods = buildMemberPaymentMethods { + missingConnection = MissingPaymentConnection.PAYOUT + } + } + }, ) + + val result = getConnectPaymentReminderUseCase.invoke() + + assertThat(result).isRight().isEqualTo(ShowConnectPayoutReminder) + } + + @Test + fun `when a contract is being terminated due to missed payments, show the missing payment reminder`() = runTest { + val getConnectPaymentReminderUseCase = GetConnectPaymentReminderUseCaseImpl(apolloClient) val terminationDate = LocalDate.parse("2030-01-01") apolloClient.enqueueTestResponse( GetPayinMethodStatusQuery(), GetPayinMethodStatusQuery.Data(OctopusFakeResolver) { currentMember = buildMember { - paymentInformation = buildMemberPaymentInformation { - status = MemberPaymentConnectionStatus.ACTIVE - } this.activeContracts = listOf( this.buildContract { this.terminationDueToMissedPayments = true @@ -94,17 +100,14 @@ class GetConnectPaymentReminderUseCaseTest { } @Test - fun `with the feature flag on but payment already connected, don't get a 'ShowReminder' response`() = runTest { - val getConnectPaymentReminderUseCase = GetConnectPaymentReminderUseCaseImpl( - apolloClient, - doesHavePayingContractsUseCaseProvider, - ) + fun `when no payment connection is missing, don't get a 'ShowReminder' response`() = runTest { + val getConnectPaymentReminderUseCase = GetConnectPaymentReminderUseCaseImpl(apolloClient) apolloClient.enqueueTestResponse( GetPayinMethodStatusQuery(), GetPayinMethodStatusQuery.Data(OctopusFakeResolver) { currentMember = buildMember { - paymentInformation = buildMemberPaymentInformation { - status = MemberPaymentConnectionStatus.ACTIVE + paymentMethods = buildMemberPaymentMethods { + missingConnection = null } } }, @@ -116,11 +119,8 @@ class GetConnectPaymentReminderUseCaseTest { } @Test - fun `with the feature flag on but network failure, don't get a 'ShowReminder' response`() = runTest { - val getConnectPaymentReminderUseCase = GetConnectPaymentReminderUseCaseImpl( - apolloClient, - doesHavePayingContractsUseCaseProvider, - ) + fun `on network failure, don't get a 'ShowReminder' response`() = runTest { + val getConnectPaymentReminderUseCase = GetConnectPaymentReminderUseCaseImpl(apolloClient) apolloClient.enqueueTestNetworkError() val result = getConnectPaymentReminderUseCase.invoke() @@ -129,34 +129,4 @@ class GetConnectPaymentReminderUseCaseTest { .isLeft() .isInstanceOf() } - - @Test - fun `for a non paying member, don't remind them to connect payment`() = runTest { - val getConnectPaymentReminderUseCase = GetConnectPaymentReminderUseCaseImpl( - apolloClient, - onlyHasNonPayingContractsUseCaseProvider, - ) - - val result = getConnectPaymentReminderUseCase.invoke() - - assertThat(result) - .isLeft() - .isInstanceOf() - } - - private val doesHavePayingContractsUseCaseProvider: Provider = Provider { - object : GetOnlyHasNonPayingContractsUseCase { - override suspend fun invoke(): Either { - return false.right() - } - } - } - - private val onlyHasNonPayingContractsUseCaseProvider: Provider = Provider { - object : GetOnlyHasNonPayingContractsUseCase { - override suspend fun invoke(): Either { - return true.right() - } - } - } -} +} \ No newline at end of file