From 97ee45839dc8b2c25b22398c6b56c5566d6b9260 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Thu, 16 Apr 2026 17:10:02 +0200 Subject: [PATCH 01/29] add manual charge button --- .../ui/payments/PaymentsDestination.kt | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) 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 f630e86423..3fd02c2b83 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 @@ -53,6 +53,7 @@ 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.Button import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority.Info @@ -355,14 +356,21 @@ private fun UpcomingPaymentInfoCard(upcomingPaymentInfo: UpcomingPaymentInfo?, m is PaymentFailed -> { val monthDateFormatter = rememberHedvigMonthDateTimeFormatter() - HedvigNotificationCard( - priority = NotificationPriority.Attention, - message = stringResource( - Res.string.PAYMENTS_MISSED_PAYMENT, - monthDateFormatter.format(upcomingPaymentInfo.failedPaymentStartDate), - monthDateFormatter.format(upcomingPaymentInfo.failedPaymentEndDate), - ), - ) + val allowManualCharge = true //todo + Column { + HedvigNotificationCard( + priority = NotificationPriority.Attention, + message = stringResource( + Res.string.PAYMENTS_MISSED_PAYMENT, + monthDateFormatter.format(upcomingPaymentInfo.failedPaymentStartDate), + monthDateFormatter.format(upcomingPaymentInfo.failedPaymentEndDate), + ), + style = if (allowManualCharge) NotificationDefaults.InfoCardStyle.Button( + "Charge failed payment manually",{} + ) else NotificationDefaults.InfoCardStyle.Default + ) + } + } null -> {} From ee6b1146a0be000ddbbd537b78c3a205ee17ca99 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Mon, 20 Apr 2026 10:29:38 +0200 Subject: [PATCH 02/29] add allowManualCharge --- .../android/feature/payments/PreviewData.kt | 1 + .../payments/data/PaymentConnection.kt | 1 + .../payments/navigation/PaymentsGraph.kt | 3 ++ .../data/GetUpcomingPaymentUseCase.kt | 2 ++ .../ui/payments/PaymentsDestination.kt | 29 +++++++++++++++---- .../payments/ui/payments/PaymentsPresenter.kt | 6 ++++ 6 files changed, 37 insertions(+), 5 deletions(-) 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 84c517f600..52237d0184 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 @@ -274,6 +274,7 @@ internal val paymentOverViewPreviewData: PaymentOverview paymentConnection = PaymentConnection.Active( displayName = "Nordea", displayValue = "31489*****", + chargeMethod = MemberPaymentChargeMethod.TRUSTLY ), ) } 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 36826876a6..6181e31c55 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 @@ -6,6 +6,7 @@ internal sealed interface PaymentConnection { data class Active( val displayName: String?, val displayValue: String?, + val chargeMethod: MemberPaymentChargeMethod ) : PaymentConnection data object Pending : PaymentConnection diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt index f41b217f16..063837736b 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt @@ -57,6 +57,9 @@ fun NavGraphBuilder.paymentsGraph( onMemberPaymentDetailsClicked = dropUnlessResumed { navController.navigate(PaymentsDestinations.MemberPaymentDetails) }, + onOpenManualCharge = { + //todo + }, ) } 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 78f9bc53b8..dce8cf0856 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 @@ -16,6 +16,7 @@ import com.hedvig.android.feature.payments.data.MemberChargeShortInfo 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.toChargeMethod import com.hedvig.android.feature.payments.data.toFailedCharge import kotlin.time.Clock import kotlin.time.Duration.Companion.days @@ -53,6 +54,7 @@ internal data class GetUpcomingPaymentUseCaseImpl( PaymentConnection.Active( displayName = paymentInformation.chargeMethod?.displayName, displayValue = paymentInformation.chargeMethod?.descriptor, + chargeMethod = paymentInformation.chargeMethod?.paymentMethod.toChargeMethod(), ) } 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 3fd02c2b83..b3433a3f94 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 @@ -114,6 +114,7 @@ internal fun PaymentsDestination( onPaymentHistoryClicked: () -> Unit, onMemberPaymentDetailsClicked: () -> Unit, onChangeBankAccount: () -> Unit, + onOpenManualCharge: () -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() PaymentsScreen( @@ -124,6 +125,7 @@ internal fun PaymentsDestination( onPaymentHistoryClicked = onPaymentHistoryClicked, onRetry = { viewModel.emit(Retry) }, onPaymentDetailsClicked = onMemberPaymentDetailsClicked, + onOpenManualCharge = onOpenManualCharge ) } @@ -135,6 +137,7 @@ private fun PaymentsScreen( onDiscountClicked: () -> Unit, onPaymentHistoryClicked: () -> Unit, onPaymentDetailsClicked: () -> Unit, + onOpenManualCharge: () -> Unit, onRetry: () -> Unit, ) { val density = LocalDensity.current @@ -196,6 +199,7 @@ private fun PaymentsScreen( onDiscountClicked = onDiscountClicked, onPaymentHistoryClicked = onPaymentHistoryClicked, onPaymentDetailsClicked = onPaymentDetailsClicked, + onOpenManualCharge = onOpenManualCharge ) Spacer(Modifier.height(16.dp)) } @@ -220,6 +224,7 @@ private fun PaymentsContent( onDiscountClicked: () -> Unit, onPaymentHistoryClicked: () -> Unit, onPaymentDetailsClicked: () -> Unit, + onOpenManualCharge: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -258,6 +263,7 @@ private fun PaymentsContent( modifier = Modifier .padding(horizontal = 16.dp) .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)), + onOpenManualCharge = onOpenManualCharge ) val showConnectedPaymentInfo = uiState is Content && uiState.connectedPaymentInfo is ConnectedPaymentInfo.NeedsSetup @@ -342,7 +348,11 @@ private fun CardNotConnectedWarningCard( } @Composable -private fun UpcomingPaymentInfoCard(upcomingPaymentInfo: UpcomingPaymentInfo?, modifier: Modifier = Modifier) { +private fun UpcomingPaymentInfoCard( + upcomingPaymentInfo: UpcomingPaymentInfo?, + onOpenManualCharge: () -> Unit, + modifier: Modifier = Modifier, +) { Box(modifier) { when (upcomingPaymentInfo) { NoInfo -> {} @@ -356,7 +366,7 @@ private fun UpcomingPaymentInfoCard(upcomingPaymentInfo: UpcomingPaymentInfo?, m is PaymentFailed -> { val monthDateFormatter = rememberHedvigMonthDateTimeFormatter() - val allowManualCharge = true //todo + val allowManualCharge = upcomingPaymentInfo.isManualChargeAllowed Column { HedvigNotificationCard( priority = NotificationPriority.Attention, @@ -365,8 +375,9 @@ private fun UpcomingPaymentInfoCard(upcomingPaymentInfo: UpcomingPaymentInfo?, m monthDateFormatter.format(upcomingPaymentInfo.failedPaymentStartDate), monthDateFormatter.format(upcomingPaymentInfo.failedPaymentEndDate), ), - style = if (allowManualCharge) NotificationDefaults.InfoCardStyle.Button( - "Charge failed payment manually",{} + style = if (allowManualCharge) Button( + buttonText = "Charge failed payment manually", //todo + onButtonClick = onOpenManualCharge ) else NotificationDefaults.InfoCardStyle.Default ) } @@ -600,6 +611,7 @@ private fun PreviewPaymentScreen( {}, {}, {}, + {} ) } } @@ -664,6 +676,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< upcomingPaymentInfo = PaymentFailed( System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, + isManualChargeAllowed = true, ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.Active( @@ -693,7 +706,11 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< System.now().toLocalDateTime(TimeZone.UTC).date, "qrdfgeth", ), - upcomingPaymentInfo = NoInfo, + upcomingPaymentInfo = PaymentFailed( + System.now().toLocalDateTime(TimeZone.UTC).date, + System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, + isManualChargeAllowed = true, + ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( null, @@ -726,6 +743,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< upcomingPaymentInfo = PaymentFailed( System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, + isManualChargeAllowed = true, ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( @@ -744,6 +762,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< upcomingPaymentInfo = PaymentFailed( System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, + isManualChargeAllowed = true, ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( 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 18c59e81aa..159323cdba 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 @@ -10,6 +10,7 @@ import androidx.compose.runtime.setValue import com.hedvig.android.core.demomode.Provider import com.hedvig.android.core.uidata.UiMoney import com.hedvig.android.feature.payments.data.MemberCharge +import com.hedvig.android.feature.payments.data.MemberPaymentChargeMethod 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.Pending @@ -64,10 +65,14 @@ internal class PaymentsPresenter( if (memberCharge?.status == MemberCharge.MemberChargeStatus.PENDING) { return@run PaymentsUiState.Content.UpcomingPaymentInfo.InProgress } + val paymentConnection = paymentOverview.paymentConnection memberCharge?.failedCharge?.let { failedCharge -> + val isManualChargeAllowed = paymentConnection is Active && + paymentConnection.chargeMethod == MemberPaymentChargeMethod.TRUSTLY return@run PaymentsUiState.Content.UpcomingPaymentInfo.PaymentFailed( failedPaymentStartDate = failedCharge.fromDate, failedPaymentEndDate = failedCharge.toDate, + isManualChargeAllowed = isManualChargeAllowed ) } PaymentsUiState.Content.UpcomingPaymentInfo.NoInfo @@ -137,6 +142,7 @@ internal sealed interface PaymentsUiState { data class PaymentFailed( val failedPaymentStartDate: LocalDate, val failedPaymentEndDate: LocalDate, + val isManualChargeAllowed: Boolean ) : UpcomingPaymentInfo } From 86e111944a805d8e25536e18d9ddd5cbd8490502 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 22 Apr 2026 11:49:59 +0200 Subject: [PATCH 03/29] add feature flag ENABLE_MANUAL_CHARGE --- .../feature/payments/data/MemberCharge.kt | 11 +- .../feature/payments/data/PaymentOverview.kt | 5 + .../feature/payments/di/PaymentsModule.kt | 7 +- .../data/GetUpcomingPaymentUseCase.kt | 147 +++++++++++------- .../payments/ui/payments/PaymentsPresenter.kt | 8 +- .../flags/UnleashFeatureFlagProvider.kt | 2 + .../android/featureflags/flags/Feature.kt | 3 +- 7 files changed, 119 insertions(+), 64 deletions(-) diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt index 99fada4411..d21c0321c4 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt @@ -3,13 +3,9 @@ package com.hedvig.android.feature.payments.data import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney import com.hedvig.android.feature.payments.data.Discount.DiscountStatus -import kotlin.String -import kotlin.time.Clock import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone import kotlinx.datetime.daysUntil import kotlinx.datetime.toJavaLocalDate -import kotlinx.datetime.todayIn import kotlinx.serialization.Serializable import octopus.PaymentHistoryWithDetailsQuery import octopus.ShortPaymentHistoryQuery @@ -47,6 +43,7 @@ internal data class MemberCharge( data class FailedCharge( val fromDate: LocalDate, val toDate: LocalDate, + val sum: UiMoney, ) internal enum class MemberChargeStatus { @@ -183,11 +180,17 @@ internal fun MemberChargeFragment.toFailedCharge(): MemberCharge.FailedCharge? { val from = previousChargesPeriods.minOfOrNull { it.fromDate } val to = previousChargesPeriods.maxOfOrNull { it.toDate } + val sum = if (previousChargesPeriods.isNotEmpty()) UiMoney( + previousChargesPeriods.sumOf { it.amount.amount }, + UiCurrencyCode.fromCurrencyCode(previousChargesPeriods.first().amount.currencyCode), + ) else + UiMoney(0.0, UiCurrencyCode.SEK) return if (from != null && to != null) { MemberCharge.FailedCharge( from, to, + sum ) } else { null 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 ee9fcfb9ed..f2cc54f665 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 @@ -7,6 +7,7 @@ internal data class PaymentOverview( val memberChargeShortInfo: MemberChargeShortInfo?, val ongoingCharges: List, val paymentConnection: PaymentConnection, + val isManualChargeAllowed: ManualChargeToPrompt? ) { data class OngoingCharge( val id: String, @@ -15,6 +16,10 @@ internal data class PaymentOverview( ) } +internal data class ManualChargeToPrompt( + val sum: UiMoney +) + internal data class MemberChargeShortInfo( val netAmount: UiMoney, val dueDate: LocalDate, diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt index 7272dbcefd..58221bb7aa 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt @@ -24,6 +24,7 @@ import com.hedvig.android.feature.payments.ui.discounts.DiscountsViewModel import com.hedvig.android.feature.payments.ui.history.PaymentHistoryViewModel import com.hedvig.android.feature.payments.ui.memberpaymentdetails.MemberPaymentDetailsViewModel import com.hedvig.android.feature.payments.ui.payments.PaymentsViewModel +import com.hedvig.android.featureflags.FeatureManager import kotlin.time.Clock import org.koin.core.module.dsl.viewModel import org.koin.dsl.module @@ -46,8 +47,9 @@ val paymentsModule = module { } single { GetUpcomingPaymentUseCaseImpl( - get(), - get(), + apolloClient = get(), + clock = get(), + featureManager = get() ) } single { @@ -109,6 +111,7 @@ val paymentsModule = module { GetUpcomingPaymentUseCaseImpl( get(), clock = get(), + featureManager = get() ) } single { 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 dce8cf0856..ca24e577d8 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 @@ -7,19 +7,33 @@ import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.cache.normalized.FetchPolicy import com.apollographql.apollo.cache.normalized.fetchPolicy import com.hedvig.android.apollo.ErrorMessage -import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.apollo.safeFlow import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.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.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.PaymentOverview import com.hedvig.android.feature.payments.data.PaymentOverview.OngoingCharge import com.hedvig.android.feature.payments.data.toChargeMethod import com.hedvig.android.feature.payments.data.toFailedCharge +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.Feature import kotlin.time.Clock import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.isActive import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import octopus.UpcomingPaymentQuery @@ -28,57 +42,83 @@ import octopus.type.MemberChargeStatus import octopus.type.MemberPaymentConnectionStatus internal interface GetUpcomingPaymentUseCase { - suspend fun invoke(): Either + suspend fun invoke(): Flow> } internal data class GetUpcomingPaymentUseCaseImpl( val apolloClient: ApolloClient, + val featureManager: FeatureManager, val clock: Clock, ) : GetUpcomingPaymentUseCase { - override suspend fun invoke(): Either = either { - val result = apolloClient.query(UpcomingPaymentQuery()) - .fetchPolicy(FetchPolicy.NetworkFirst) - .safeExecute(::ErrorMessage) - .bind() - - PaymentOverview( - memberChargeShortInfo = result.currentMember.futureCharge?.toMemberChargeShortInfo(), - ongoingCharges = result.currentMember.ongoingCharges.mapNotNull { - val id = it.id ?: return@mapNotNull null - OngoingCharge(id, it.date, UiMoney.fromMoneyFragment(it.net)) + override suspend fun invoke(): Flow> { + return combine( + flow { + while (currentCoroutineContext().isActive) { + emitAll( + apolloClient.query(UpcomingPaymentQuery()) + .fetchPolicy(FetchPolicy.NetworkFirst) + .safeFlow(::ErrorMessage), + ) + delay(3.seconds) + } }, - paymentConnection = run { - val paymentInformation = result.currentMember.paymentInformation - when (paymentInformation.status) { - MemberPaymentConnectionStatus.ACTIVE -> { - PaymentConnection.Active( - displayName = paymentInformation.chargeMethod?.displayName, - displayValue = paymentInformation.chargeMethod?.descriptor, - chargeMethod = paymentInformation.chargeMethod?.paymentMethod.toChargeMethod(), - ) - } + featureManager.isFeatureEnabled(Feature.ENABLE_MANUAL_CHARGE), + ) { response, isManualChargeFlagEnabled -> + either { + val result = response.bind() + val paymentConnection = run { + val paymentInformation = result.currentMember.paymentInformation + when (paymentInformation.status) { + MemberPaymentConnectionStatus.ACTIVE -> { + PaymentConnection.Active( + displayName = paymentInformation.chargeMethod?.displayName, + displayValue = paymentInformation.chargeMethod?.descriptor, + chargeMethod = paymentInformation.chargeMethod?.paymentMethod.toChargeMethod(), + ) + } - MemberPaymentConnectionStatus.PENDING -> { - PaymentConnection.Pending - } + MemberPaymentConnectionStatus.PENDING -> { + PaymentConnection.Pending + } - MemberPaymentConnectionStatus.NEEDS_SETUP -> { - val firstKnownTerminationDateForContractTerminatedDueToMissedPayments = result - .currentMember - .activeContracts - .filter { it.terminationDueToMissedPayments } - .mapNotNull { it.terminationDate } - .sorted() - .firstOrNull() - PaymentConnection.NeedsSetup(firstKnownTerminationDateForContractTerminatedDueToMissedPayments) - } + MemberPaymentConnectionStatus.NEEDS_SETUP -> { + val firstKnownTerminationDateForContractTerminatedDueToMissedPayments = result + .currentMember + .activeContracts + .filter { it.terminationDueToMissedPayments } + .mapNotNull { it.terminationDate } + .sorted() + .firstOrNull() + PaymentConnection.NeedsSetup(firstKnownTerminationDateForContractTerminatedDueToMissedPayments) + } - MemberPaymentConnectionStatus.UNKNOWN__ -> { - PaymentConnection.Unknown + MemberPaymentConnectionStatus.UNKNOWN__ -> { + PaymentConnection.Unknown + } } } - }, - ) + val memberChargeShortInfo = result.currentMember.futureCharge?.toMemberChargeShortInfo() + + val isManualChargeAllowed = if ( + isManualChargeFlagEnabled && + paymentConnection is Active && + paymentConnection.chargeMethod == MemberPaymentChargeMethod.TRUSTLY && + memberChargeShortInfo?.failedCharge != null + ) ManualChargeToPrompt( + memberChargeShortInfo.failedCharge.sum, + ) else null + + PaymentOverview( + memberChargeShortInfo = memberChargeShortInfo, + ongoingCharges = result.currentMember.ongoingCharges.mapNotNull { + val id = it.id ?: return@mapNotNull null + OngoingCharge(id, it.date, UiMoney.fromMoneyFragment(it.net)) + }, + isManualChargeAllowed = isManualChargeAllowed, + paymentConnection = paymentConnection, + ) + } + } } } @@ -99,17 +139,20 @@ private fun MemberChargeFragment.toMemberChargeShortInfo() = MemberChargeShortIn internal class GetUpcomingPaymentUseCaseDemo( private val clock: Clock, ) : GetUpcomingPaymentUseCase { - override suspend fun invoke(): Either { - return PaymentOverview( - MemberChargeShortInfo( - netAmount = UiMoney(100.0, UiCurrencyCode.SEK), - id = "id", - status = MemberCharge.MemberChargeStatus.SUCCESS, - dueDate = (clock.now() + 10.days).toLocalDateTime(TimeZone.UTC).date, - failedCharge = null, - ), - emptyList(), - PaymentConnection.Unknown, - ).right() + override suspend fun invoke(): Flow> { + return flowOf( + PaymentOverview( + MemberChargeShortInfo( + netAmount = UiMoney(100.0, UiCurrencyCode.SEK), + id = "id", + status = MemberCharge.MemberChargeStatus.SUCCESS, + dueDate = (clock.now() + 10.days).toLocalDateTime(TimeZone.UTC).date, + failedCharge = null, + ), + emptyList(), + PaymentConnection.Unknown, + isManualChargeAllowed = null, + ).right(), + ) } } 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 159323cdba..4293dcef0c 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 @@ -9,6 +9,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.hedvig.android.core.demomode.Provider import com.hedvig.android.core.uidata.UiMoney +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.Active @@ -65,14 +66,11 @@ internal class PaymentsPresenter( if (memberCharge?.status == MemberCharge.MemberChargeStatus.PENDING) { return@run PaymentsUiState.Content.UpcomingPaymentInfo.InProgress } - val paymentConnection = paymentOverview.paymentConnection memberCharge?.failedCharge?.let { failedCharge -> - val isManualChargeAllowed = paymentConnection is Active && - paymentConnection.chargeMethod == MemberPaymentChargeMethod.TRUSTLY return@run PaymentsUiState.Content.UpcomingPaymentInfo.PaymentFailed( failedPaymentStartDate = failedCharge.fromDate, failedPaymentEndDate = failedCharge.toDate, - isManualChargeAllowed = isManualChargeAllowed + isManualChargeAllowed = paymentOverview.isManualChargeAllowed ) } PaymentsUiState.Content.UpcomingPaymentInfo.NoInfo @@ -142,7 +140,7 @@ internal sealed interface PaymentsUiState { data class PaymentFailed( val failedPaymentStartDate: LocalDate, val failedPaymentEndDate: LocalDate, - val isManualChargeAllowed: Boolean + val isManualChargeAllowed: ManualChargeToPrompt? ) : UpcomingPaymentInfo } diff --git a/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt b/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt index 0040e59b9a..59aa1c36cf 100644 --- a/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt +++ b/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt @@ -37,6 +37,8 @@ internal class UnleashFeatureFlagProvider( Feature.DISABLE_REDEEM_CAMPAIGN -> hedvigUnleashClient.client.isEnabled("disable_redeem_campaign", false) Feature.ENABLE_CLAIM_HISTORY -> hedvigUnleashClient.client.isEnabled("enable_claim_history", false) + + Feature.ENABLE_MANUAL_CHARGE -> hedvigUnleashClient.client.isEnabled("enable_manual_charge") } }.distinctUntilChanged() } 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 c50489b0e0..f5747c4238 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 @@ -21,5 +21,6 @@ enum class Feature( "When enabled, it allows the chat to show media in inline video players in the chat messages", ), DISABLE_REDEEM_CAMPAIGN("Disables the ability to redeem a campaign code"), - ENABLE_CLAIM_HISTORY("Disables the ability to redeem a campaign code"), + ENABLE_CLAIM_HISTORY("Enables claim history"), + ENABLE_MANUAL_CHARGE("Enables manual self-charge for member in the app payments") } From 7e639b4fa48f163e25c6e0b243339a8b4b7472a9 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 22 Apr 2026 11:58:35 +0200 Subject: [PATCH 04/29] fix ui with new api --- .../android/feature/payments/PreviewData.kt | 9 ++ .../ui/payments/PaymentsDestination.kt | 21 +++- .../payments/ui/payments/PaymentsPresenter.kt | 108 +++++++++--------- 3 files changed, 81 insertions(+), 57 deletions(-) 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 52237d0184..844478d20b 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 @@ -3,6 +3,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.feature.payments.data.Discount +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.MemberPaymentChargeMethod @@ -98,6 +99,7 @@ internal val chargeHistoryPreviewData = listOf( failedCharge = MemberCharge.FailedCharge( fromDate = LocalDate.fromEpochDays(200), toDate = LocalDate.fromEpochDays(201), + sum = UiMoney(200.0, UiCurrencyCode.SEK), ), chargeBreakdowns = listOf( MemberCharge.ChargeBreakdown( @@ -131,6 +133,7 @@ internal val chargeHistoryPreviewData = listOf( failedCharge = MemberCharge.FailedCharge( fromDate = LocalDate.fromEpochDays(200), toDate = LocalDate.fromEpochDays(201), + UiMoney(200.0, UiCurrencyCode.SEK) ), chargeBreakdowns = listOf( MemberCharge.ChargeBreakdown( @@ -164,6 +167,7 @@ internal val chargeHistoryPreviewData = listOf( failedCharge = MemberCharge.FailedCharge( fromDate = LocalDate.fromEpochDays(200), toDate = LocalDate.fromEpochDays(201), + UiMoney(200.0, UiCurrencyCode.SEK) ), chargeBreakdowns = listOf( MemberCharge.ChargeBreakdown( @@ -197,6 +201,7 @@ internal val chargeHistoryPreviewData = listOf( failedCharge = MemberCharge.FailedCharge( fromDate = LocalDate.fromEpochDays(200), toDate = LocalDate.fromEpochDays(201), + UiMoney(200.0, UiCurrencyCode.SEK) ), chargeBreakdowns = listOf( MemberCharge.ChargeBreakdown( @@ -230,6 +235,7 @@ internal val chargeHistoryPreviewData = listOf( failedCharge = MemberCharge.FailedCharge( fromDate = LocalDate.fromEpochDays(200), toDate = LocalDate.fromEpochDays(201), + UiMoney(200.0, UiCurrencyCode.SEK) ), chargeBreakdowns = listOf( MemberCharge.ChargeBreakdown( @@ -266,6 +272,7 @@ internal val paymentOverViewPreviewData: PaymentOverview failedCharge = MemberCharge.FailedCharge( fromDate = LocalDate.fromEpochDays(200), toDate = LocalDate.fromEpochDays(201), + sum = UiMoney(200.0, UiCurrencyCode.SEK), ), ) return PaymentOverview( @@ -276,6 +283,7 @@ internal val paymentOverViewPreviewData: PaymentOverview displayValue = "31489*****", chargeMethod = MemberPaymentChargeMethod.TRUSTLY ), + isManualChargeAllowed = ManualChargeToPrompt(UiMoney(200.0, UiCurrencyCode.SEK)) ) } @@ -288,6 +296,7 @@ internal val paymentDetailsPreviewData = MemberCharge( failedCharge = MemberCharge.FailedCharge( fromDate = LocalDate.fromEpochDays(200), toDate = LocalDate.fromEpochDays(201), + sum = UiMoney(200.0, UiCurrencyCode.SEK), ), chargeBreakdowns = listOf( MemberCharge.ChargeBreakdown( 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 b3433a3f94..c60668f33c 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 @@ -39,6 +39,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.design.system.hedvig.ButtonDefaults @@ -67,6 +68,7 @@ import com.hedvig.android.design.system.hedvig.placeholder.hedvigPlaceholder import com.hedvig.android.design.system.hedvig.placeholder.shimmer import com.hedvig.android.design.system.hedvig.rememberHedvigDateTimeFormatter import com.hedvig.android.design.system.hedvig.rememberHedvigMonthDateTimeFormatter +import com.hedvig.android.feature.payments.data.ManualChargeToPrompt import com.hedvig.android.feature.payments.data.PaymentOverview.OngoingCharge import com.hedvig.android.feature.payments.ui.payments.PaymentsEvent.Retry import com.hedvig.android.feature.payments.ui.payments.PaymentsUiState.Content @@ -366,7 +368,6 @@ private fun UpcomingPaymentInfoCard( is PaymentFailed -> { val monthDateFormatter = rememberHedvigMonthDateTimeFormatter() - val allowManualCharge = upcomingPaymentInfo.isManualChargeAllowed Column { HedvigNotificationCard( priority = NotificationPriority.Attention, @@ -375,7 +376,7 @@ private fun UpcomingPaymentInfoCard( monthDateFormatter.format(upcomingPaymentInfo.failedPaymentStartDate), monthDateFormatter.format(upcomingPaymentInfo.failedPaymentEndDate), ), - style = if (allowManualCharge) Button( + style = if (upcomingPaymentInfo.isManualChargeAllowed!=null) Button( buttonText = "Charge failed payment manually", //todo onButtonClick = onOpenManualCharge ) else NotificationDefaults.InfoCardStyle.Default @@ -676,7 +677,9 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< upcomingPaymentInfo = PaymentFailed( System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, - isManualChargeAllowed = true, + isManualChargeAllowed = ManualChargeToPrompt( + UiMoney(200.0, UiCurrencyCode.SEK) + ), ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.Active( @@ -709,7 +712,9 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< upcomingPaymentInfo = PaymentFailed( System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, - isManualChargeAllowed = true, + isManualChargeAllowed = ManualChargeToPrompt( + UiMoney(200.0, UiCurrencyCode.SEK) + ), ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( @@ -743,7 +748,9 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< upcomingPaymentInfo = PaymentFailed( System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, - isManualChargeAllowed = true, + isManualChargeAllowed = ManualChargeToPrompt( + UiMoney(200.0, UiCurrencyCode.SEK) + ), ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( @@ -762,7 +769,9 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< upcomingPaymentInfo = PaymentFailed( System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, - isManualChargeAllowed = true, + isManualChargeAllowed = ManualChargeToPrompt( + UiMoney(200.0, UiCurrencyCode.SEK) + ), ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( 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 4293dcef0c..313ffe9027 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 @@ -20,6 +20,7 @@ import com.hedvig.android.feature.payments.data.PaymentOverview.OngoingCharge import com.hedvig.android.feature.payments.overview.data.GetUpcomingPaymentUseCase import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope +import kotlinx.coroutines.flow.collectLatest import kotlinx.datetime.LocalDate internal class PaymentsPresenter( @@ -47,60 +48,65 @@ internal class PaymentsPresenter( PaymentsUiState.Loading } } - getUpcomingPaymentUseCase.provide().invoke().fold( - ifLeft = { - paymentsUiState = PaymentsUiState.Error - }, - ifRight = { paymentOverview -> - paymentsUiState = PaymentsUiState.Content( - isRetrying = false, - upcomingPayment = paymentOverview.memberChargeShortInfo?.let { memberCharge -> - PaymentsUiState.Content.UpcomingPayment.Content( - netAmount = memberCharge.netAmount, - dueDate = memberCharge.dueDate, - id = memberCharge.id, - ) - } ?: PaymentsUiState.Content.UpcomingPayment.NoUpcomingPayment, - upcomingPaymentInfo = run { - val memberCharge = paymentOverview.memberChargeShortInfo - if (memberCharge?.status == MemberCharge.MemberChargeStatus.PENDING) { - return@run PaymentsUiState.Content.UpcomingPaymentInfo.InProgress - } - memberCharge?.failedCharge?.let { failedCharge -> - return@run PaymentsUiState.Content.UpcomingPaymentInfo.PaymentFailed( - failedPaymentStartDate = failedCharge.fromDate, - failedPaymentEndDate = failedCharge.toDate, - isManualChargeAllowed = paymentOverview.isManualChargeAllowed - ) - } - PaymentsUiState.Content.UpcomingPaymentInfo.NoInfo + getUpcomingPaymentUseCase.provide().invoke() + .collectLatest { result -> + result.fold( + ifLeft = { + paymentsUiState = PaymentsUiState.Error }, - ongoingCharges = paymentOverview.ongoingCharges, - connectedPaymentInfo = when (val paymentConnection = paymentOverview.paymentConnection) { - is Active -> { - PaymentsUiState.Content.ConnectedPaymentInfo.Active( - displayName = paymentConnection.displayName, - maskedAccountNumber = paymentConnection.displayValue, - ) - } - - Pending -> { - PaymentsUiState.Content.ConnectedPaymentInfo.Pending - } - - is NeedsSetup -> { - PaymentsUiState.Content.ConnectedPaymentInfo.NeedsSetup( - dueDateToConnect = paymentConnection.terminationDateIfNotConnected, - ) - } - - Unknown -> { - PaymentsUiState.Content.ConnectedPaymentInfo.Unknown - } + ifRight = { paymentOverview -> + paymentsUiState = PaymentsUiState.Content( + isRetrying = false, + upcomingPayment = paymentOverview.memberChargeShortInfo?.let { memberCharge -> + PaymentsUiState.Content.UpcomingPayment.Content( + netAmount = memberCharge.netAmount, + dueDate = memberCharge.dueDate, + id = memberCharge.id, + ) + } ?: PaymentsUiState.Content.UpcomingPayment.NoUpcomingPayment, + upcomingPaymentInfo = run { + val memberCharge = paymentOverview.memberChargeShortInfo + if (memberCharge?.status == MemberCharge.MemberChargeStatus.PENDING) { + return@run PaymentsUiState.Content.UpcomingPaymentInfo.InProgress + } + memberCharge?.failedCharge?.let { failedCharge -> + return@run PaymentsUiState.Content.UpcomingPaymentInfo.PaymentFailed( + failedPaymentStartDate = failedCharge.fromDate, + failedPaymentEndDate = failedCharge.toDate, + isManualChargeAllowed = paymentOverview.isManualChargeAllowed + ) + } + PaymentsUiState.Content.UpcomingPaymentInfo.NoInfo + }, + ongoingCharges = paymentOverview.ongoingCharges, + connectedPaymentInfo = when (val paymentConnection = paymentOverview.paymentConnection) { + is Active -> { + PaymentsUiState.Content.ConnectedPaymentInfo.Active( + displayName = paymentConnection.displayName, + maskedAccountNumber = paymentConnection.displayValue, + ) + } + + Pending -> { + PaymentsUiState.Content.ConnectedPaymentInfo.Pending + } + + is NeedsSetup -> { + PaymentsUiState.Content.ConnectedPaymentInfo.NeedsSetup( + dueDateToConnect = paymentConnection.terminationDateIfNotConnected, + ) + } + + Unknown -> { + PaymentsUiState.Content.ConnectedPaymentInfo.Unknown + } + }, + ) }, ) - }, - ) + } + + } return paymentsUiState } From 4f63c780f6cfa34f8fed14165aed6f8db9c3e614 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Thu, 23 Apr 2026 21:01:23 +0200 Subject: [PATCH 05/29] add box --- .../android/feature/payments/PreviewData.kt | 12 +- .../feature/payments/data/MemberCharge.kt | 13 +- .../payments/data/PaymentConnection.kt | 2 +- .../feature/payments/data/PaymentOverview.kt | 4 +- .../feature/payments/di/PaymentsModule.kt | 4 +- .../payments/navigation/PaymentsGraph.kt | 2 +- .../data/GetUpcomingPaymentUseCase.kt | 10 +- .../MemberPaymentDetailsDestination.kt | 8 +- .../ui/payments/PaymentsDestination.kt | 125 ++++++++++++++++-- .../payments/ui/payments/PaymentsPresenter.kt | 6 +- 10 files changed, 145 insertions(+), 41 deletions(-) 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 844478d20b..81694493c5 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 @@ -133,7 +133,7 @@ internal val chargeHistoryPreviewData = listOf( failedCharge = MemberCharge.FailedCharge( fromDate = LocalDate.fromEpochDays(200), toDate = LocalDate.fromEpochDays(201), - UiMoney(200.0, UiCurrencyCode.SEK) + UiMoney(200.0, UiCurrencyCode.SEK), ), chargeBreakdowns = listOf( MemberCharge.ChargeBreakdown( @@ -167,7 +167,7 @@ internal val chargeHistoryPreviewData = listOf( failedCharge = MemberCharge.FailedCharge( fromDate = LocalDate.fromEpochDays(200), toDate = LocalDate.fromEpochDays(201), - UiMoney(200.0, UiCurrencyCode.SEK) + UiMoney(200.0, UiCurrencyCode.SEK), ), chargeBreakdowns = listOf( MemberCharge.ChargeBreakdown( @@ -201,7 +201,7 @@ internal val chargeHistoryPreviewData = listOf( failedCharge = MemberCharge.FailedCharge( fromDate = LocalDate.fromEpochDays(200), toDate = LocalDate.fromEpochDays(201), - UiMoney(200.0, UiCurrencyCode.SEK) + UiMoney(200.0, UiCurrencyCode.SEK), ), chargeBreakdowns = listOf( MemberCharge.ChargeBreakdown( @@ -235,7 +235,7 @@ internal val chargeHistoryPreviewData = listOf( failedCharge = MemberCharge.FailedCharge( fromDate = LocalDate.fromEpochDays(200), toDate = LocalDate.fromEpochDays(201), - UiMoney(200.0, UiCurrencyCode.SEK) + UiMoney(200.0, UiCurrencyCode.SEK), ), chargeBreakdowns = listOf( MemberCharge.ChargeBreakdown( @@ -281,9 +281,9 @@ internal val paymentOverViewPreviewData: PaymentOverview paymentConnection = PaymentConnection.Active( displayName = "Nordea", displayValue = "31489*****", - chargeMethod = MemberPaymentChargeMethod.TRUSTLY + chargeMethod = MemberPaymentChargeMethod.TRUSTLY, ), - isManualChargeAllowed = ManualChargeToPrompt(UiMoney(200.0, UiCurrencyCode.SEK)) + isManualChargeAllowed = ManualChargeToPrompt(UiMoney(200.0, UiCurrencyCode.SEK)), ) } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt index d21c0321c4..3a36ee4fae 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt @@ -180,17 +180,20 @@ internal fun MemberChargeFragment.toFailedCharge(): MemberCharge.FailedCharge? { val from = previousChargesPeriods.minOfOrNull { it.fromDate } val to = previousChargesPeriods.maxOfOrNull { it.toDate } - val sum = if (previousChargesPeriods.isNotEmpty()) UiMoney( - previousChargesPeriods.sumOf { it.amount.amount }, - UiCurrencyCode.fromCurrencyCode(previousChargesPeriods.first().amount.currencyCode), - ) else + val sum = if (previousChargesPeriods.isNotEmpty()) { + UiMoney( + previousChargesPeriods.sumOf { it.amount.amount }, + UiCurrencyCode.fromCurrencyCode(previousChargesPeriods.first().amount.currencyCode), + ) + } else { UiMoney(0.0, UiCurrencyCode.SEK) + } return if (from != null && to != null) { MemberCharge.FailedCharge( from, to, - sum + sum, ) } else { null 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 6181e31c55..11b6e9a56f 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 @@ -6,7 +6,7 @@ internal sealed interface PaymentConnection { data class Active( val displayName: String?, val displayValue: String?, - val chargeMethod: MemberPaymentChargeMethod + val chargeMethod: MemberPaymentChargeMethod, ) : PaymentConnection data object Pending : 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 f2cc54f665..01804abc00 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 @@ -7,7 +7,7 @@ internal data class PaymentOverview( val memberChargeShortInfo: MemberChargeShortInfo?, val ongoingCharges: List, val paymentConnection: PaymentConnection, - val isManualChargeAllowed: ManualChargeToPrompt? + val isManualChargeAllowed: ManualChargeToPrompt?, ) { data class OngoingCharge( val id: String, @@ -17,7 +17,7 @@ internal data class PaymentOverview( } internal data class ManualChargeToPrompt( - val sum: UiMoney + val sum: UiMoney, ) internal data class MemberChargeShortInfo( diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt index 58221bb7aa..174d62c627 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt @@ -49,7 +49,7 @@ val paymentsModule = module { GetUpcomingPaymentUseCaseImpl( apolloClient = get(), clock = get(), - featureManager = get() + featureManager = get(), ) } single { @@ -111,7 +111,7 @@ val paymentsModule = module { GetUpcomingPaymentUseCaseImpl( get(), clock = get(), - featureManager = get() + featureManager = get(), ) } single { diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt index 063837736b..3d0b6c5268 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt @@ -58,7 +58,7 @@ fun NavGraphBuilder.paymentsGraph( navController.navigate(PaymentsDestinations.MemberPaymentDetails) }, onOpenManualCharge = { - //todo + // todo }, ) } 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 ca24e577d8..7717756ef7 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 @@ -104,9 +104,13 @@ internal data class GetUpcomingPaymentUseCaseImpl( paymentConnection is Active && paymentConnection.chargeMethod == MemberPaymentChargeMethod.TRUSTLY && memberChargeShortInfo?.failedCharge != null - ) ManualChargeToPrompt( - memberChargeShortInfo.failedCharge.sum, - ) else null + ) { + ManualChargeToPrompt( + memberChargeShortInfo.failedCharge.sum, + ) + } else { + null + } PaymentOverview( memberChargeShortInfo = memberChargeShortInfo, diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsDestination.kt index dd2a405127..5791961bc6 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsDestination.kt @@ -115,9 +115,11 @@ private fun MemberPaymentDetailsSuccessScreen( onChangeBankAccount: () -> Unit, modifier: Modifier = Modifier, ) { - Column(modifier - .padding(horizontal = 16.dp) - .verticalScroll(rememberScrollState())) { + Column( + modifier + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()), + ) { val explanationBottomSheetState = rememberHedvigBottomSheetState() ExplanationBottomSheet(explanationBottomSheetState) HorizontalItemsWithMaximumSpaceTaken( 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 c60668f33c..61ee959556 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 @@ -3,6 +3,7 @@ package com.hedvig.android.feature.payments.ui.payments import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.expandVertically +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -24,6 +25,7 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsTopHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -64,6 +66,7 @@ import com.hedvig.android.design.system.hedvig.icon.Card import com.hedvig.android.design.system.hedvig.icon.ChevronRight import com.hedvig.android.design.system.hedvig.icon.Clock import com.hedvig.android.design.system.hedvig.icon.HedvigIcons +import com.hedvig.android.design.system.hedvig.icon.WarningFilled import com.hedvig.android.design.system.hedvig.placeholder.hedvigPlaceholder import com.hedvig.android.design.system.hedvig.placeholder.shimmer import com.hedvig.android.design.system.hedvig.rememberHedvigDateTimeFormatter @@ -127,7 +130,7 @@ internal fun PaymentsDestination( onPaymentHistoryClicked = onPaymentHistoryClicked, onRetry = { viewModel.emit(Retry) }, onPaymentDetailsClicked = onMemberPaymentDetailsClicked, - onOpenManualCharge = onOpenManualCharge + onOpenManualCharge = onOpenManualCharge, ) } @@ -201,7 +204,7 @@ private fun PaymentsScreen( onDiscountClicked = onDiscountClicked, onPaymentHistoryClicked = onPaymentHistoryClicked, onPaymentDetailsClicked = onPaymentDetailsClicked, - onOpenManualCharge = onOpenManualCharge + onOpenManualCharge = onOpenManualCharge, ) Spacer(Modifier.height(16.dp)) } @@ -265,7 +268,7 @@ private fun PaymentsContent( modifier = Modifier .padding(horizontal = 16.dp) .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)), - onOpenManualCharge = onOpenManualCharge + onOpenManualCharge = onOpenManualCharge, ) val showConnectedPaymentInfo = uiState is Content && uiState.connectedPaymentInfo is ConnectedPaymentInfo.NeedsSetup @@ -315,7 +318,7 @@ private fun PaymentsContent( is ConnectedPaymentInfo.NeedsSetup, ConnectedPaymentInfo.Unknown, is ConnectedPaymentInfo.Active, - -> { + -> { } } } @@ -376,13 +379,16 @@ private fun UpcomingPaymentInfoCard( monthDateFormatter.format(upcomingPaymentInfo.failedPaymentStartDate), monthDateFormatter.format(upcomingPaymentInfo.failedPaymentEndDate), ), - style = if (upcomingPaymentInfo.isManualChargeAllowed!=null) Button( - buttonText = "Charge failed payment manually", //todo - onButtonClick = onOpenManualCharge - ) else NotificationDefaults.InfoCardStyle.Default + style = if (upcomingPaymentInfo.isManualChargeAllowed != null) { + Button( + buttonText = "Charge failed payment manually", // todo + onButtonClick = onOpenManualCharge, + ) + } else { + NotificationDefaults.InfoCardStyle.Default + }, ) } - } null -> {} @@ -574,6 +580,83 @@ private fun PaymentCard( } } +@Composable +private fun FailedPaymentInfo(amountDue: String, onReviewPaymentClick: () -> Unit, modifier: Modifier = Modifier) { + HedvigCard( + color = HedvigTheme.colorScheme.fillNegative, + borderColor = HedvigTheme.colorScheme.borderPrimary, + modifier = modifier + .fillMaxWidth() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + Box( + modifier = Modifier + .background( + color = HedvigTheme.colorScheme.signalRedFill, + shape = CircleShape, + ) + .padding(8.dp), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = HedvigIcons.WarningFilled, + contentDescription = null, + tint = HedvigTheme.colorScheme.signalRedElement, + modifier = Modifier.size(24.dp), + ) + } + Column( + modifier = Modifier + .weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + HedvigText( + // todo text + text = "Payment overdue", + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textPrimary, + ) + HedvigText( + // todo text + text = "Amount due: $amountDue", + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondary, + ) + } + } + Spacer(Modifier.height(8.dp)) + HedvigText( + // todo text + text = "We couldn't collect this payment from your bank account. Pay now to keep your insurance active.", + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondary, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + ) + + Spacer(Modifier.height(12.dp)) + HedvigButton( + text = "Review payment", + onClick = onReviewPaymentClick, + enabled = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + buttonSize = ButtonDefaults.ButtonSize.Small, + ) + } + } +} + @Composable private fun PaymentsListItem( text: String, @@ -597,6 +680,20 @@ private fun PaymentsListItem( ) } +@Composable +@HedvigPreview +private fun PreviewFailedPaymentInfo() { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + FailedPaymentInfo( + amountDue = "233", + {}, + ) + } + } +} + + @Composable @HedvigPreview private fun PreviewPaymentScreen( @@ -612,7 +709,7 @@ private fun PreviewPaymentScreen( {}, {}, {}, - {} + {}, ) } } @@ -678,7 +775,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, isManualChargeAllowed = ManualChargeToPrompt( - UiMoney(200.0, UiCurrencyCode.SEK) + UiMoney(200.0, UiCurrencyCode.SEK), ), ), ongoingCharges = emptyList(), @@ -713,7 +810,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, isManualChargeAllowed = ManualChargeToPrompt( - UiMoney(200.0, UiCurrencyCode.SEK) + UiMoney(200.0, UiCurrencyCode.SEK), ), ), ongoingCharges = emptyList(), @@ -749,7 +846,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, isManualChargeAllowed = ManualChargeToPrompt( - UiMoney(200.0, UiCurrencyCode.SEK) + UiMoney(200.0, UiCurrencyCode.SEK), ), ), ongoingCharges = emptyList(), @@ -770,7 +867,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, isManualChargeAllowed = ManualChargeToPrompt( - UiMoney(200.0, UiCurrencyCode.SEK) + UiMoney(200.0, UiCurrencyCode.SEK), ), ), ongoingCharges = emptyList(), 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 313ffe9027..a1f8b673e5 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 @@ -73,7 +73,7 @@ internal class PaymentsPresenter( return@run PaymentsUiState.Content.UpcomingPaymentInfo.PaymentFailed( failedPaymentStartDate = failedCharge.fromDate, failedPaymentEndDate = failedCharge.toDate, - isManualChargeAllowed = paymentOverview.isManualChargeAllowed + isManualChargeAllowed = paymentOverview.isManualChargeAllowed, ) } PaymentsUiState.Content.UpcomingPaymentInfo.NoInfo @@ -105,8 +105,6 @@ internal class PaymentsPresenter( }, ) } - - } return paymentsUiState } @@ -146,7 +144,7 @@ internal sealed interface PaymentsUiState { data class PaymentFailed( val failedPaymentStartDate: LocalDate, val failedPaymentEndDate: LocalDate, - val isManualChargeAllowed: ManualChargeToPrompt? + val isManualChargeAllowed: ManualChargeToPrompt?, ) : UpcomingPaymentInfo } From 226d3ffe43dc33f7990e402c28c9af78461dce3c Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Thu, 23 Apr 2026 21:52:00 +0200 Subject: [PATCH 06/29] fux box border and padding --- .../ui/payments/PaymentsDestination.kt | 56 ++++++++++++------- 1 file changed, 35 insertions(+), 21 deletions(-) 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 61ee959556..3023d6b494 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 @@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.expandVertically import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -32,6 +33,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics @@ -61,6 +63,7 @@ import com.hedvig.android.design.system.hedvig.NotificationDefaults.InfoCardStyl 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.hedvigDropShadow import com.hedvig.android.design.system.hedvig.icon.Campaign import com.hedvig.android.design.system.hedvig.icon.Card import com.hedvig.android.design.system.hedvig.icon.ChevronRight @@ -238,6 +241,20 @@ private fun PaymentsContent( horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(Modifier.height(8.dp)) + when (val upcomingPaymentInfo = (uiState as? Content)?.upcomingPaymentInfo) { + is PaymentFailed -> { + if (upcomingPaymentInfo.isManualChargeAllowed != null) { + FailedPaymentInfo( + amountDue = upcomingPaymentInfo.isManualChargeAllowed.sum.toString(), + onReviewPaymentClick = onOpenManualCharge, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Spacer(Modifier.height(8.dp)) + } + } + + else -> {} + } val ongoingCharges = (uiState as? Content)?.ongoingCharges if (!ongoingCharges.isNullOrEmpty()) { OngoingPaymentCards( @@ -268,7 +285,6 @@ private fun PaymentsContent( modifier = Modifier .padding(horizontal = 16.dp) .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)), - onOpenManualCharge = onOpenManualCharge, ) val showConnectedPaymentInfo = uiState is Content && uiState.connectedPaymentInfo is ConnectedPaymentInfo.NeedsSetup @@ -355,7 +371,6 @@ private fun CardNotConnectedWarningCard( @Composable private fun UpcomingPaymentInfoCard( upcomingPaymentInfo: UpcomingPaymentInfo?, - onOpenManualCharge: () -> Unit, modifier: Modifier = Modifier, ) { Box(modifier) { @@ -371,23 +386,18 @@ private fun UpcomingPaymentInfoCard( is PaymentFailed -> { val monthDateFormatter = rememberHedvigMonthDateTimeFormatter() - Column { - HedvigNotificationCard( - priority = NotificationPriority.Attention, - message = stringResource( - Res.string.PAYMENTS_MISSED_PAYMENT, - monthDateFormatter.format(upcomingPaymentInfo.failedPaymentStartDate), - monthDateFormatter.format(upcomingPaymentInfo.failedPaymentEndDate), - ), - style = if (upcomingPaymentInfo.isManualChargeAllowed != null) { - Button( - buttonText = "Charge failed payment manually", // todo - onButtonClick = onOpenManualCharge, - ) - } else { - NotificationDefaults.InfoCardStyle.Default - }, - ) + if (upcomingPaymentInfo.isManualChargeAllowed == null) { + Column { + HedvigNotificationCard( + priority = NotificationPriority.Attention, + message = stringResource( + Res.string.PAYMENTS_MISSED_PAYMENT, + monthDateFormatter.format(upcomingPaymentInfo.failedPaymentStartDate), + monthDateFormatter.format(upcomingPaymentInfo.failedPaymentEndDate), + ), + style = NotificationDefaults.InfoCardStyle.Default, + ) + } } } @@ -584,9 +594,11 @@ private fun PaymentCard( private fun FailedPaymentInfo(amountDue: String, onReviewPaymentClick: () -> Unit, modifier: Modifier = Modifier) { HedvigCard( color = HedvigTheme.colorScheme.fillNegative, - borderColor = HedvigTheme.colorScheme.borderPrimary, modifier = modifier .fillMaxWidth() + .border(1.dp, HedvigTheme.colorScheme.borderPrimary, + HedvigTheme.shapes.cornerXLarge) + .hedvigDropShadow() ) { Column( modifier = Modifier @@ -640,7 +652,9 @@ private fun FailedPaymentInfo(amountDue: String, onReviewPaymentClick: () -> Uni text = "We couldn't collect this payment from your bank account. Pay now to keep your insurance active.", style = HedvigTheme.typography.label, color = HedvigTheme.colorScheme.textSecondary, - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), ) Spacer(Modifier.height(12.dp)) From a62c86fcb16c82bb70db01c6a314ea2ee5de63d2 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 24 Apr 2026 12:09:26 +0200 Subject: [PATCH 07/29] ManualChargeScreen WIP --- .../navigation/PaymentsDestination.kt | 4 + .../payments/navigation/PaymentsGraph.kt | 12 + .../manualcharge/ManualChargeDestination.kt | 263 ++++++++++++++++++ .../ui/manualcharge/ManualChargeViewModel.kt | 53 ++++ .../core/HedvigDeepLinkContainer.kt | 7 + 5 files changed, 339 insertions(+) create mode 100644 app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt create mode 100644 app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt index 2e4eb9d1a8..11f7bccad2 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt @@ -28,4 +28,8 @@ internal sealed interface PaymentsDestinations { @Serializable data object MemberPaymentDetails : PaymentsDestinations, Destination + + @Serializable + data object ManualCharge: PaymentsDestinations, Destination } + diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt index 3d0b6c5268..290ccae948 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt @@ -12,6 +12,8 @@ import com.hedvig.android.feature.payments.ui.discounts.DiscountsDestination import com.hedvig.android.feature.payments.ui.discounts.DiscountsViewModel import com.hedvig.android.feature.payments.ui.history.PaymentHistoryDestination import com.hedvig.android.feature.payments.ui.history.PaymentHistoryViewModel +import com.hedvig.android.feature.payments.ui.manualcharge.ManualChargeDestination +import com.hedvig.android.feature.payments.ui.manualcharge.ManualChargeViewModel import com.hedvig.android.feature.payments.ui.memberpaymentdetails.MemberPaymentDetailsDestination import com.hedvig.android.feature.payments.ui.memberpaymentdetails.MemberPaymentDetailsViewModel import com.hedvig.android.feature.payments.ui.payments.PaymentsDestination @@ -63,6 +65,16 @@ fun NavGraphBuilder.paymentsGraph( ) } + navdestination( + deepLinks = navDeepLinks(hedvigDeepLinkContainer.manualCharge), + ) { + val viewModel: ManualChargeViewModel = koinViewModel() + ManualChargeDestination( + viewModel = viewModel, + navigateUp = navController::navigateUp, + ) + } + navdestination { val viewModel: PaymentDetailsViewModel = koinViewModel(parameters = { parametersOf(this.memberChargeId) }) PaymentDetailsDestination( diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt new file mode 100644 index 0000000000..7f45b7889f --- /dev/null +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt @@ -0,0 +1,263 @@ +package com.hedvig.android.feature.payments.ui.manualcharge + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.hedvig.android.core.uidata.UiCurrencyCode +import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.design.system.hedvig.ButtonDefaults +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.HedvigPreview +import com.hedvig.android.design.system.hedvig.HedvigScaffold +import com.hedvig.android.design.system.hedvig.HedvigText +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.HorizontalDivider +import com.hedvig.android.design.system.hedvig.Icon +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.hedvigDropShadow +import com.hedvig.android.design.system.hedvig.icon.HedvigIcons +import com.hedvig.android.design.system.hedvig.icon.WarningFilled +import com.hedvig.android.design.system.hedvig.rememberHedvigDateTimeFormatter +import kotlinx.datetime.LocalDate + +@Composable +internal fun ManualChargeDestination(viewModel: ManualChargeViewModel, navigateUp: () -> Unit) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle() + + ManualChargeScreen( + uiState = uiState.value, + navigateUp = navigateUp, + reload = { viewModel.emit(ManualChargeEvent.Retry) }, + ) +} + +@Composable +private fun ManualChargeScreen( + uiState: ManualChargeUiState, + navigateUp: () -> Unit, + reload: () -> Unit, +) { + HedvigScaffold( + navigateUp = navigateUp, + topAppBarText = "Payment overdue", //todo + ) { + when (uiState) { + + is ManualChargeUiState.Failure -> { + //todo + HedvigErrorSection( + onButtonClick = reload, + Modifier.weight(1f), + ) + + } + + ManualChargeUiState.Loading -> { + HedvigFullScreenCenterAlignedProgress( + modifier = Modifier.weight(1f), + ) + } + + is ManualChargeUiState.Success -> { + ManualChargeSuccessScreen(uiState) + } + } + } +} + +@Composable +private fun ManualChargeSuccessScreen(uiState: ManualChargeUiState.Success) { + val dateTimeFormatter = rememberHedvigDateTimeFormatter() + Column( + modifier = Modifier + .padding(16.dp) + .hedvigDropShadow(HedvigTheme.shapes.cornerXLarge) + .fillMaxWidth() + .background( + color = HedvigTheme.colorScheme.backgroundPrimary, + shape = HedvigTheme.shapes.cornerXLarge, + ) + .border( + width = 1.dp, + color = HedvigTheme.colorScheme.borderPrimary, + shape = HedvigTheme.shapes.cornerXLarge, + ) + + .clip(HedvigTheme.shapes.cornerXLarge) + .padding(16.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + verticalAlignment = Alignment.Top, + ) { + Icon( + imageVector = HedvigIcons.WarningFilled, + contentDescription = null, + tint = HedvigTheme.colorScheme.signalRedElement, + modifier = Modifier.size(40.dp), + ) + Spacer(modifier = Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + HedvigText( + text = "Overdue since ${dateTimeFormatter.format(uiState.dueDate)}", + ) + HedvigText( + text = "Pay now to avoid interruption", + color = HedvigTheme.colorScheme.textSecondary, + ) + } + } + + HedvigButton( + text = "View payment details", + onClick = { /* TODO: Navigate to payment details */ }, + enabled = true, + modifier = Modifier.fillMaxWidth(), + buttonStyle = ButtonDefaults.ButtonStyle.Secondary, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + HedvigText( + text = "Due date", + color = HedvigTheme.colorScheme.textSecondary, + ) + HedvigText( + text = dateTimeFormatter.format(uiState.dueDate), + color = HedvigTheme.colorScheme.textSecondary, + ) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + HedvigText( + text = "Bank account", + color = HedvigTheme.colorScheme.textSecondary, + ) + HedvigText( + text = "*** *3242", + color = HedvigTheme.colorScheme.textSecondary, + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider() + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + HedvigText( + text = "Total", + ) + HedvigText( + text = uiState.amount.toString(), + textAlign = TextAlign.End, + ) + } + + HedvigButton( + text = "Pay ${uiState.amount}", + onClick = { /* TODO: Handle payment */ }, + enabled = true, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(8.dp)) + HedvigText( + text = "Ensure your account has enough\nfunds to cover this payment", + color = HedvigTheme.colorScheme.textTertiary, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + +} + +@Composable +@Preview +@HedvigPreview +private fun ManualChargeScreenSuccessPreview() { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + ManualChargeScreen( + uiState = ManualChargeUiState.Success( + dueDate = LocalDate(2026, 1, 1), + amount = UiMoney(100.0, UiCurrencyCode.SEK), + ), + navigateUp = {}, + reload = {}, + ) + } + } +} + +@Composable +@Preview +@HedvigPreview +private fun ManualChargeScreenLoadingPreview() { + HedvigTheme { + Surface { + ManualChargeScreen( + uiState = ManualChargeUiState.Loading, + navigateUp = {}, + reload = {}, + ) + } + } +} + +@Composable +@Preview +@HedvigPreview +private fun ManualChargeScreenFailurePreview() { + HedvigTheme { + Surface { + ManualChargeScreen( + uiState = ManualChargeUiState.Failure(ManualChargeFailureReason.GeneralFailure), + navigateUp = {}, + reload = {}, + ) + } + } +} + + diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt new file mode 100644 index 0000000000..b926b434f3 --- /dev/null +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt @@ -0,0 +1,53 @@ +package com.hedvig.android.feature.payments.ui.manualcharge + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope +import com.hedvig.android.molecule.public.MoleculeViewModel +import kotlinx.datetime.LocalDate + +internal class ManualChargeViewModel : MoleculeViewModel( + initialState = ManualChargeUiState.Loading, + presenter = ManualChargePresenter(), +) + +private class ManualChargePresenter : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present( + lastState: ManualChargeUiState, + ): ManualChargeUiState { + // TODO: Implement presenter logic + return ManualChargeUiState.Loading + } +} + +internal sealed interface ManualChargeUiState { + data object Loading : ManualChargeUiState + + data class Failure( + val reason: ManualChargeFailureReason + ) : ManualChargeUiState + + data class Success( + val dueDate: LocalDate, + val amount: UiMoney + ) : ManualChargeUiState +} + +internal interface ManualChargeFailureReason { + data object NotAllowed: ManualChargeFailureReason + data object GeneralFailure: ManualChargeFailureReason + data class UserErrorWithMessage( + val message: String + ): ManualChargeFailureReason +} + +internal sealed interface ManualChargeEvent { + data object Retry : ManualChargeEvent + + // TODO: Add events +} + diff --git a/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt b/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt index 650003a628..6bf0b02b73 100644 --- a/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt +++ b/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt @@ -79,6 +79,8 @@ interface HedvigDeepLinkContainer { val petIdWithoutContractId: List val petIdWithContractId: List + + val manualCharge: List } internal class HedvigDeepLinkContainerImpl( @@ -196,6 +198,10 @@ internal class HedvigDeepLinkContainerImpl( override val petIdWithContractId: List = baseDeepLinkDomains.map { baseDeepLinkDomain -> "$baseDeepLinkDomain/pet-id?contractId={contractId}" } + + override val manualCharge: List = baseDeepLinkDomains.map { baseDeepLinkDomain -> + "$baseDeepLinkDomain/manual-charge" //todo: check with other platforms + } } val HedvigDeepLinkContainer.allDeepLinkUriPatterns: List @@ -235,4 +241,5 @@ val HedvigDeepLinkContainer.allDeepLinkUriPatterns: List travelAddonWithContractId.first(), petIdWithoutContractId.first(), petIdWithContractId.first(), + manualCharge.first() ) From 53a3339b32ad28c1bf0bcdcf9907ac3276c0c717 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 24 Apr 2026 13:16:17 +0200 Subject: [PATCH 08/29] ManualChargeScreen and string in PaymentsDestination --- .../androidMain/res/values-sv-rSE/strings.xml | 16 ++++++-- .../src/androidMain/res/values/strings.xml | 16 ++++++-- .../values-sv-rSE/strings.xml | 16 ++++++-- .../composeResources/values/strings.xml | 16 ++++++-- .../manualcharge/ManualChargeDestination.kt | 38 +++++++++++++------ .../ui/payments/PaymentsDestination.kt | 17 +++++---- 6 files changed, 83 insertions(+), 36 deletions(-) diff --git a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml index 7cea7bbfd9..dd99c1296e 100644 --- a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml @@ -80,8 +80,7 @@ Skanna QR-koden med BankID-appen på din telefon, eller logga in med din e-postadress nedan. Utbetalning till ett svenskt bankkonto Bankkonto - Clearing - Direktutbetalning via Trustly + Clearingnummer Hej - just nu har vi lunch-stängt i chatten. Vi svarar så fort vi kan när vi öppnar igen klockan 13. Tack för ditt meddelande. Vi svarar så snart som möjligt. Just nu är chatten stängd. Vi svarar så fort vi kan när vi öppnar. @@ -147,7 +146,7 @@ Ange byggår Byggår Du + %1$d - Ändra konto + Ändra utbetalningskonto Ändra skyddsnivå Chatta med en specialist Om det inte fungerar eller om du har några andra frågor, vänligen meddela oss så hjälper en av våra specialister till inom kort! @@ -517,7 +516,7 @@ Fakturan skickas till din Kivra-inkorg 14 dagar före förfallodatumet varje månad. Tillgänglighet Juridisk information - Juridisk information + Legal information Personuppgifter Fråga angående skadeanmälan - Fordon reg. %1$s Välj land och språk @@ -609,6 +608,10 @@ Denna betalning misslyckades och lades till i din betalning den %1$s . Betalningshistorik Betalningssätt + Att betala: %1$s + Vi kunde inte dra betalningen från ditt bankkonto. Betala för att undvika avbrott i ditt skydd. + Granska betalning + Försenad betalning Betalning genomförd %1$s dagar Hel period @@ -622,6 +625,11 @@ Kommande betalning Giltig till %1$s Betalningshistorik + Lägg till utbetalningsmetod + Direktutbetalning via Faktura + Snabb utbetalning med Swish + Utbetalning via Trustly + Du har inte lagt till någon utbetalningsmetod än. Lägg till en för att vi ska kunna betala ut till dig. Utbetalningskonto Välj utbetalningsmetod Vi behöver tillåtelse att spela in ljud diff --git a/app/core/core-resources/src/androidMain/res/values/strings.xml b/app/core/core-resources/src/androidMain/res/values/strings.xml index 335c64edc2..b5e2cc6770 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -78,10 +78,9 @@ We use AI to help you as quickly as possible. The answers are generated automatically and may not always be correct.\n\nIf necessary, you’ll be connected to our service team to help you further.\n\nThe details of your insurance are outlined in your insurance letter and terms and conditions. AI and automated messages Scan the QR-code with the BankID app on the phone where it’s installed or log in with email using the button below. - Payout via a Swedish bank account + Payout to a Swedish bank account Bank account - Clearing - Direct payment via Trustly + Clearing number Hej - just nu har vi lunch-stängt i chatten. Vi svarar så fort vi kan när vi öppnar igen klockan 13. Thank you for reaching out to us. We will answer as soon as possible. Hej - just nu är chatten stängd. Vi svarar så fort vi kan när vi öppnar. @@ -147,7 +146,7 @@ Please enter year of construction Year of construction You + %1$d - Change account + Change payout account Change coverage level Talk to a human If it’s not working or if you have any other questions, please let us know and one of our specialists will help out shortly! @@ -609,6 +608,10 @@ This payment failed and was added to your payment on %1$s. Payment history Payment method + Amount due: %1$s + We couldn\'t collect this payment from your bank account. Pay now to keep your coverage active. + Review payment + Payment overdue Payment successful %1$s days Full period @@ -622,6 +625,11 @@ Upcoming payment Valid until %1$s Payment history + Add payout method + Direct payment via Invoice + Instant payout with Swish + Payout via Trustly + You haven’t added a payout method yet. Add one to receive payouts. Payout account Choose payout method We need permission to record audio diff --git a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml index 416af9b0a8..d398180b35 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -80,8 +80,7 @@ Skanna QR-koden med BankID-appen på din telefon, eller logga in med din e-postadress nedan. Utbetalning till ett svenskt bankkonto Bankkonto - Clearing - Direktutbetalning via Trustly + Clearingnummer Hej - just nu har vi lunch-stängt i chatten. Vi svarar så fort vi kan när vi öppnar igen klockan 13. Tack för ditt meddelande. Vi svarar så snart som möjligt. Just nu är chatten stängd. Vi svarar så fort vi kan när vi öppnar. @@ -147,7 +146,7 @@ Ange byggår Byggår Du + %1$d - Ändra konto + Ändra utbetalningskonto Ändra skyddsnivå Chatta med en specialist Om det inte fungerar eller om du har några andra frågor, vänligen meddela oss så hjälper en av våra specialister till inom kort! @@ -517,7 +516,7 @@ Fakturan skickas till din Kivra-inkorg 14 dagar före förfallodatumet varje månad. Tillgänglighet Juridisk information - Juridisk information + Legal information Personuppgifter Fråga angående skadeanmälan - Fordon reg. %1$s Välj land och språk @@ -609,6 +608,10 @@ Denna betalning misslyckades och lades till i din betalning den %1$s . Betalningshistorik Betalningssätt + Att betala: %1$s + Vi kunde inte dra betalningen från ditt bankkonto. Betala för att undvika avbrott i ditt skydd. + Granska betalning + Försenad betalning Betalning genomförd %1$s dagar Hel period @@ -622,6 +625,11 @@ Kommande betalning Giltig till %1$s Betalningshistorik + Lägg till utbetalningsmetod + Direktutbetalning via Faktura + Snabb utbetalning med Swish + Utbetalning via Trustly + Du har inte lagt till någon utbetalningsmetod än. Lägg till en för att vi ska kunna betala ut till dig. Utbetalningskonto Välj utbetalningsmetod Vi behöver tillåtelse att spela in ljud diff --git a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml index f684d45a2e..cd1461124e 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -78,10 +78,9 @@ We use AI to help you as quickly as possible. The answers are generated automatically and may not always be correct.\n\nIf necessary, you’ll be connected to our service team to help you further.\n\nThe details of your insurance are outlined in your insurance letter and terms and conditions. AI and automated messages Scan the QR-code with the BankID app on the phone where it’s installed or log in with email using the button below. - Payout via a Swedish bank account + Payout to a Swedish bank account Bank account - Clearing - Direct payment via Trustly + Clearing number Hej - just nu har vi lunch-stängt i chatten. Vi svarar så fort vi kan när vi öppnar igen klockan 13. Thank you for reaching out to us. We will answer as soon as possible. Hej - just nu är chatten stängd. Vi svarar så fort vi kan när vi öppnar. @@ -147,7 +146,7 @@ Please enter year of construction Year of construction You + %1$d - Change account + Change payout account Change coverage level Talk to a human If it’s not working or if you have any other questions, please let us know and one of our specialists will help out shortly! @@ -609,6 +608,10 @@ This payment failed and was added to your payment on %1$s. Payment history Payment method + Amount due: %1$s + We couldn't collect this payment from your bank account. Pay now to keep your coverage active. + Review payment + Payment overdue Payment successful %1$s days Full period @@ -622,6 +625,11 @@ Upcoming payment Valid until %1$s Payment history + Add payout method + Direct payment via Invoice + Instant payout with Swish + Payout via Trustly + You haven’t added a payout method yet. Add one to receive payouts. Payout account Choose payout method We need permission to record audio diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt index 7f45b7889f..46d4ce7049 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt @@ -15,6 +15,7 @@ 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.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -22,6 +23,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney import com.hedvig.android.design.system.hedvig.ButtonDefaults +import com.hedvig.android.design.system.hedvig.DropdownDefaults import com.hedvig.android.design.system.hedvig.HedvigButton import com.hedvig.android.design.system.hedvig.HedvigErrorSection import com.hedvig.android.design.system.hedvig.HedvigFullScreenCenterAlignedProgress @@ -35,8 +37,13 @@ import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.design.system.hedvig.hedvigDropShadow import com.hedvig.android.design.system.hedvig.icon.HedvigIcons import com.hedvig.android.design.system.hedvig.icon.WarningFilled +import com.hedvig.android.design.system.hedvig.rememberHedvigBirthDateDateTimeFormatter import com.hedvig.android.design.system.hedvig.rememberHedvigDateTimeFormatter +import com.hedvig.android.design.system.hedvig.rememberHedvigMonthDateTimeFormatter +import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_TITLE +import hedvig.resources.Res import kotlinx.datetime.LocalDate +import org.jetbrains.compose.resources.stringResource @Composable internal fun ManualChargeDestination(viewModel: ManualChargeViewModel, navigateUp: () -> Unit) { @@ -57,7 +64,7 @@ private fun ManualChargeScreen( ) { HedvigScaffold( navigateUp = navigateUp, - topAppBarText = "Payment overdue", //todo + topAppBarText = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_TITLE), ) { when (uiState) { @@ -85,7 +92,8 @@ private fun ManualChargeScreen( @Composable private fun ManualChargeSuccessScreen(uiState: ManualChargeUiState.Success) { - val dateTimeFormatter = rememberHedvigDateTimeFormatter() + val dateTimeFormatter = rememberHedvigMonthDateTimeFormatter() + val dateTimeFormatterWithYear = rememberHedvigDateTimeFormatter() Column( modifier = Modifier .padding(16.dp) @@ -119,7 +127,7 @@ private fun ManualChargeSuccessScreen(uiState: ManualChargeUiState.Success) { Spacer(modifier = Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { HedvigText( - text = "Overdue since ${dateTimeFormatter.format(uiState.dueDate)}", + text = "Overdue since ${dateTimeFormatter.format (uiState.dueDate)}", ) HedvigText( text = "Pay now to avoid interruption", @@ -133,7 +141,9 @@ private fun ManualChargeSuccessScreen(uiState: ManualChargeUiState.Success) { onClick = { /* TODO: Navigate to payment details */ }, enabled = true, modifier = Modifier.fillMaxWidth(), - buttonStyle = ButtonDefaults.ButtonStyle.Secondary, + buttonStyle = ButtonDefaults.ButtonStyle.Ghost, + buttonSize = ButtonDefaults.ButtonSize.Medium, + border = HedvigTheme.colorScheme.borderPrimary ) Spacer(modifier = Modifier.height(16.dp)) @@ -143,31 +153,34 @@ private fun ManualChargeSuccessScreen(uiState: ManualChargeUiState.Success) { ) { Row( modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp), + .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, ) { HedvigText( - text = "Due date", + text = "Due date", //todo color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label ) HedvigText( - text = dateTimeFormatter.format(uiState.dueDate), + text = dateTimeFormatterWithYear.format(uiState.dueDate), color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label ) } - + Spacer( Modifier.height(10.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, ) { HedvigText( - text = "Bank account", + text = "Bank account", //todo color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label ) HedvigText( - text = "*** *3242", + text = "*** *3242", //todo color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label, ) } } @@ -202,8 +215,9 @@ private fun ManualChargeSuccessScreen(uiState: ManualChargeUiState.Success) { Spacer(modifier = Modifier.height(8.dp)) HedvigText( text = "Ensure your account has enough\nfunds to cover this payment", - color = HedvigTheme.colorScheme.textTertiary, + color = HedvigTheme.colorScheme.textSecondaryTranslucent, textAlign = TextAlign.Center, + style = HedvigTheme.typography.label, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), 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 3023d6b494..a1da610631 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 @@ -99,6 +99,10 @@ import hedvig.resources.PAYMENTS_MISSED_PAYMENT import hedvig.resources.PAYMENTS_NO_PAYMENTS_IN_PROGRESS import hedvig.resources.PAYMENTS_PAYMENT_DETAILS_INFO_TITLE import hedvig.resources.PAYMENTS_PAYMENT_HISTORY_BUTTON_LABEL +import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_AMOUNT_DUE +import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_BODY +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.PROFILE_PAYMENT_CONNECT_DIRECT_DEBIT_TITLE @@ -633,14 +637,12 @@ private fun FailedPaymentInfo(amountDue: String, onReviewPaymentClick: () -> Uni verticalArrangement = Arrangement.spacedBy(2.dp), ) { HedvigText( - // todo text - text = "Payment overdue", + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_TITLE), style = HedvigTheme.typography.label, color = HedvigTheme.colorScheme.textPrimary, ) HedvigText( - // todo text - text = "Amount due: $amountDue", + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_AMOUNT_DUE, amountDue), style = HedvigTheme.typography.label, color = HedvigTheme.colorScheme.textSecondary, ) @@ -648,8 +650,7 @@ private fun FailedPaymentInfo(amountDue: String, onReviewPaymentClick: () -> Uni } Spacer(Modifier.height(8.dp)) HedvigText( - // todo text - text = "We couldn't collect this payment from your bank account. Pay now to keep your insurance active.", + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_BODY), style = HedvigTheme.typography.label, color = HedvigTheme.colorScheme.textSecondary, modifier = Modifier @@ -659,7 +660,7 @@ private fun FailedPaymentInfo(amountDue: String, onReviewPaymentClick: () -> Uni Spacer(Modifier.height(12.dp)) HedvigButton( - text = "Review payment", + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_BUTTON), onClick = onReviewPaymentClick, enabled = true, modifier = Modifier @@ -700,7 +701,7 @@ private fun PreviewFailedPaymentInfo() { HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { FailedPaymentInfo( - amountDue = "233", + amountDue = "233 kr", {}, ) } From 3f8a73609a50405c29ef20443db3a31701f0563a Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 24 Apr 2026 15:32:54 +0200 Subject: [PATCH 09/29] add GetManualChargeInfoUseCase and strings --- .../androidMain/res/values-sv-rSE/strings.xml | 6 + .../src/androidMain/res/values/strings.xml | 6 + .../values-sv-rSE/strings.xml | 6 + .../composeResources/values/strings.xml | 6 + .../graphql/QueryManualChargeInfo.graphql | 33 +++++ .../data/GetManualChargeInfoUseCase.kt | 88 ++++++++++++ .../feature/payments/data/MemberCharge.kt | 3 +- .../data/TriggerManualChargeUseCase.kt | 11 ++ .../feature/payments/di/PaymentsModule.kt | 23 ++++ .../payments/navigation/PaymentsGraph.kt | 7 + .../manualcharge/ManualChargeDestination.kt | 129 +++++++++++------- .../ui/manualcharge/ManualChargeViewModel.kt | 20 ++- 12 files changed, 284 insertions(+), 54 deletions(-) create mode 100644 app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql create mode 100644 app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt create mode 100644 app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt diff --git a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml index dd99c1296e..a61bb30445 100644 --- a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml @@ -611,6 +611,12 @@ Att betala: %1$s Vi kunde inte dra betalningen från ditt bankkonto. Betala för att undvika avbrott i ditt skydd. Granska betalning + Betala för att undvika avbrott + Förfallodag + Se till att det finns tillräckliga medel på kontot för att genomföra betalningen + Betala %1$s + Försenad sedan %1$s + Visa betalningsinformation Försenad betalning Betalning genomförd %1$s dagar diff --git a/app/core/core-resources/src/androidMain/res/values/strings.xml b/app/core/core-resources/src/androidMain/res/values/strings.xml index b5e2cc6770..22400fff12 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -611,6 +611,12 @@ Amount due: %1$s We couldn\'t collect this payment from your bank account. Pay now to keep your coverage active. Review payment + Pay now to avoid interruption + Payment due + Ensure your account has enough funds to cover this payment + Pay %1$s + Overdue since %1$s + View payment details Payment overdue Payment successful %1$s days diff --git a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml index d398180b35..dfaf17c780 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -611,6 +611,12 @@ Att betala: %1$s Vi kunde inte dra betalningen från ditt bankkonto. Betala för att undvika avbrott i ditt skydd. Granska betalning + Betala för att undvika avbrott + Förfallodag + Se till att det finns tillräckliga medel på kontot för att genomföra betalningen + Betala %1$s + Försenad sedan %1$s + Visa betalningsinformation Försenad betalning Betalning genomförd %1$s dagar diff --git a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml index cd1461124e..73bff99a7c 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -611,6 +611,12 @@ Amount due: %1$s We couldn't collect this payment from your bank account. Pay now to keep your coverage active. Review payment + Pay now to avoid interruption + Payment due + Ensure your account has enough funds to cover this payment + Pay %1$s + Overdue since %1$s + View payment details Payment overdue Payment successful %1$s days diff --git a/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql b/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql new file mode 100644 index 0000000000..5104fcf9e1 --- /dev/null +++ b/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql @@ -0,0 +1,33 @@ +query ManualChargeInfo { + currentMember { + futureCharge { + ...on MemberCharge { + chargeBreakdown { + periods { + isPreviouslyFailedCharge + } + } + } + + } + pastCharges { + ...on MemberCharge { + id + date + status + net { + ...MoneyFragment + } + } + } + + paymentInformation { + status + chargeMethod { + displayName + descriptor + paymentMethod + } + } + } +} diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt new file mode 100644 index 0000000000..58c619f025 --- /dev/null +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt @@ -0,0 +1,88 @@ +package com.hedvig.android.feature.payments.data + +import arrow.core.Either +import arrow.core.raise.context.bind +import arrow.core.raise.context.either +import arrow.core.raise.context.raise +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.cache.normalized.FetchPolicy.NetworkFirst +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.uidata.UiMoney +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.Feature +import kotlinx.coroutines.flow.first +import kotlinx.datetime.LocalDate +import octopus.ManualChargeInfoQuery +import octopus.type.MemberChargeStatus +import com.hedvig.android.logger.logcat +import octopus.type.MemberPaymentConnectionStatus + +internal interface GetManualChargeInfoUseCase { + suspend fun invoke(): Either +} + +internal class GetManualChargeInfoUseCaseImpl( + private val apolloClient: ApolloClient, + private val featureManager: FeatureManager, +): GetManualChargeInfoUseCase { + override suspend fun invoke(): Either = either { + + val isFeatureEnabled = featureManager.isFeatureEnabled(Feature.ENABLE_MANUAL_CHARGE).first() + if (!isFeatureEnabled) { + logcat {"GetManualChargeInfoUseCaseImpl: manual charge FF is off"} + raise(ErrorMessage()) + } + + val currentMember = apolloClient.query(ManualChargeInfoQuery()) + .fetchPolicy(NetworkFirst) + .safeExecute(::ErrorMessage) + .bind() + .currentMember + + val isPaymentMethodTrustly = currentMember.paymentInformation.status == MemberPaymentConnectionStatus.ACTIVE && + currentMember.paymentInformation.chargeMethod?.paymentMethod.toChargeMethod() == + MemberPaymentChargeMethod.TRUSTLY + + if (!isPaymentMethodTrustly) { + logcat {"GetManualChargeInfoUseCaseImpl: payment method not Trustly"} + raise(ErrorMessage()) + } + + val isFailedInUpcomingPayment = currentMember.futureCharge?.chargeBreakdown + ?.flatMap { it.periods } + ?.any { it.isPreviouslyFailedCharge } == true + if (!isFailedInUpcomingPayment) { + logcat {"GetManualChargeInfoUseCaseImpl: no failed in upcoming payment"} + raise(ErrorMessage()) + } + + val latestFailedPastCharge = currentMember.pastCharges + .maxByOrNull { it.date } + .takeIf { it?.status == MemberChargeStatus.FAILED } + + if (latestFailedPastCharge==null) { + logcat {"GetManualChargeInfoUseCaseImpl: latestFailedPastCharge is null"} + raise(ErrorMessage()) + } + + ManualChargeInfo( + chargeId = latestFailedPastCharge.id, + missedDueDate = latestFailedPastCharge.date, + amountDue = UiMoney.fromMoneyFragment(latestFailedPastCharge.net), + bankAccountDisplayValue = currentMember.paymentInformation.chargeMethod?.displayName, + bankDescriptor = currentMember.paymentInformation.chargeMethod?.descriptor + ) + } +//TODO: all these flags will be moved to BE +} + +internal data class ManualChargeInfo( + val chargeId: String?, + val missedDueDate: LocalDate, + val amountDue: UiMoney, + val bankDescriptor: String?, + val bankAccountDisplayValue: String? +) diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt index 3a36ee4fae..22625c6236 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt @@ -167,7 +167,8 @@ internal fun MemberChargeFragment.toMemberCharge( internal fun String?.toChargeMethod(): MemberPaymentChargeMethod { return when { - this?.startsWith("kivra", ignoreCase = true) == true -> MemberPaymentChargeMethod.KIVRA + this?.startsWith("kivra", ignoreCase = true) == true || + this?.startsWith("invoice", ignoreCase = true) == true -> MemberPaymentChargeMethod.KIVRA this?.startsWith("trustly", ignoreCase = true) == true -> MemberPaymentChargeMethod.TRUSTLY else -> MemberPaymentChargeMethod.UNKNOWN } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt new file mode 100644 index 0000000000..68aa5b5bd9 --- /dev/null +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt @@ -0,0 +1,11 @@ +package com.hedvig.android.feature.payments.data + +internal interface TriggerManualChargeUseCase { + suspend fun invoke() +} + +internal class TriggerManualChargeUseCaseImpl: TriggerManualChargeUseCase { + override suspend fun invoke() { + TODO("Not yet implemented") + } +} diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt index 174d62c627..5878915422 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt @@ -9,10 +9,14 @@ import com.hedvig.android.feature.payments.data.GetDiscountsOverviewUseCase import com.hedvig.android.feature.payments.data.GetDiscountsOverviewUseCaseImpl import com.hedvig.android.feature.payments.data.GetDiscountsUseCase import com.hedvig.android.feature.payments.data.GetDiscountsUseCaseImpl +import com.hedvig.android.feature.payments.data.GetManualChargeInfoUseCase +import com.hedvig.android.feature.payments.data.GetManualChargeInfoUseCaseImpl import com.hedvig.android.feature.payments.data.GetMemberPaymentsDetailsUseCase import com.hedvig.android.feature.payments.data.GetMemberPaymentsDetailsUseCaseImpl import com.hedvig.android.feature.payments.data.GetPaymentsHistoryUseCase import com.hedvig.android.feature.payments.data.GetPaymentsHistoryUseCaseImpl +import com.hedvig.android.feature.payments.data.TriggerManualChargeUseCase +import com.hedvig.android.feature.payments.data.TriggerManualChargeUseCaseImpl import com.hedvig.android.feature.payments.overview.data.GetForeverInformationUseCase import com.hedvig.android.feature.payments.overview.data.GetForeverInformationUseCaseImpl import com.hedvig.android.feature.payments.overview.data.GetUpcomingPaymentUseCase @@ -22,6 +26,7 @@ import com.hedvig.android.feature.payments.overview.data.GetUpcomingPaymentUseCa import com.hedvig.android.feature.payments.ui.details.PaymentDetailsViewModel import com.hedvig.android.feature.payments.ui.discounts.DiscountsViewModel import com.hedvig.android.feature.payments.ui.history.PaymentHistoryViewModel +import com.hedvig.android.feature.payments.ui.manualcharge.ManualChargeViewModel import com.hedvig.android.feature.payments.ui.memberpaymentdetails.MemberPaymentDetailsViewModel import com.hedvig.android.feature.payments.ui.payments.PaymentsViewModel import com.hedvig.android.featureflags.FeatureManager @@ -119,4 +124,22 @@ val paymentsModule = module { clock = get(), ) } + + single { + TriggerManualChargeUseCaseImpl() + } + + single { + GetManualChargeInfoUseCaseImpl( + get(), + get(), + ) + } + + viewModel { + ManualChargeViewModel( + get(), + get(), + ) + } } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt index 290ccae948..e7fc379892 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt @@ -72,6 +72,13 @@ fun NavGraphBuilder.paymentsGraph( ManualChargeDestination( viewModel = viewModel, navigateUp = navController::navigateUp, + onNavigateToPaymentDetails = dropUnlessResumed { chargeId: String -> + navController.navigate( + PaymentsDestinations.Details( + chargeId, + ), + ) + } ) } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt index 46d4ce7049..5a506e7e01 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt @@ -15,7 +15,6 @@ 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.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -23,7 +22,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney import com.hedvig.android.design.system.hedvig.ButtonDefaults -import com.hedvig.android.design.system.hedvig.DropdownDefaults import com.hedvig.android.design.system.hedvig.HedvigButton import com.hedvig.android.design.system.hedvig.HedvigErrorSection import com.hedvig.android.design.system.hedvig.HedvigFullScreenCenterAlignedProgress @@ -37,22 +35,34 @@ import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.design.system.hedvig.hedvigDropShadow import com.hedvig.android.design.system.hedvig.icon.HedvigIcons import com.hedvig.android.design.system.hedvig.icon.WarningFilled -import com.hedvig.android.design.system.hedvig.rememberHedvigBirthDateDateTimeFormatter import com.hedvig.android.design.system.hedvig.rememberHedvigDateTimeFormatter import com.hedvig.android.design.system.hedvig.rememberHedvigMonthDateTimeFormatter +import com.hedvig.android.feature.payments.data.ManualChargeInfo +import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_BODY +import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_DUE_DATE +import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_FINE_PRINT +import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_PAY +import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_SINCE +import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_VIEW_DETAILS import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_TITLE import hedvig.resources.Res +import hedvig.resources.payment_details_receipt_card_total import kotlinx.datetime.LocalDate import org.jetbrains.compose.resources.stringResource @Composable -internal fun ManualChargeDestination(viewModel: ManualChargeViewModel, navigateUp: () -> Unit) { +internal fun ManualChargeDestination( + viewModel: ManualChargeViewModel, + navigateUp: () -> Unit, + onNavigateToPaymentDetails: (chargeId: String) -> Unit, +) { val uiState = viewModel.uiState.collectAsStateWithLifecycle() ManualChargeScreen( uiState = uiState.value, navigateUp = navigateUp, reload = { viewModel.emit(ManualChargeEvent.Retry) }, + onNavigateToPaymentDetails = onNavigateToPaymentDetails ) } @@ -61,6 +71,7 @@ private fun ManualChargeScreen( uiState: ManualChargeUiState, navigateUp: () -> Unit, reload: () -> Unit, + onNavigateToPaymentDetails: (chargeId: String) -> Unit, ) { HedvigScaffold( navigateUp = navigateUp, @@ -84,19 +95,30 @@ private fun ManualChargeScreen( } is ManualChargeUiState.Success -> { - ManualChargeSuccessScreen(uiState) + ManualChargeSuccessScreen( + uiState, + onNavigateToPaymentDetails = onNavigateToPaymentDetails + ) } } } } @Composable -private fun ManualChargeSuccessScreen(uiState: ManualChargeUiState.Success) { +private fun ManualChargeSuccessScreen( + uiState: ManualChargeUiState.Success, + onNavigateToPaymentDetails: (chargeId: String) -> Unit, +) { val dateTimeFormatter = rememberHedvigMonthDateTimeFormatter() val dateTimeFormatterWithYear = rememberHedvigDateTimeFormatter() Column( modifier = Modifier - .padding(16.dp) + .padding( + top = 8.dp, + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ) .hedvigDropShadow(HedvigTheme.shapes.cornerXLarge) .fillMaxWidth() .background( @@ -127,27 +149,31 @@ private fun ManualChargeSuccessScreen(uiState: ManualChargeUiState.Success) { Spacer(modifier = Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { HedvigText( - text = "Overdue since ${dateTimeFormatter.format (uiState.dueDate)}", + text = stringResource( + Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_SINCE, + dateTimeFormatter.format(uiState.manualChargeInfo.missedDueDate), + ), ) HedvigText( - text = "Pay now to avoid interruption", + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_BODY), color = HedvigTheme.colorScheme.textSecondary, ) } } - - HedvigButton( - text = "View payment details", - onClick = { /* TODO: Navigate to payment details */ }, - enabled = true, - modifier = Modifier.fillMaxWidth(), - buttonStyle = ButtonDefaults.ButtonStyle.Ghost, - buttonSize = ButtonDefaults.ButtonSize.Medium, - border = HedvigTheme.colorScheme.borderPrimary - ) - + if (uiState.manualChargeInfo.chargeId!=null) { + HedvigButton( + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_VIEW_DETAILS), + onClick = { + onNavigateToPaymentDetails(uiState.manualChargeInfo.chargeId) + }, + enabled = true, + modifier = Modifier.fillMaxWidth(), + buttonStyle = ButtonDefaults.ButtonStyle.Ghost, + buttonSize = ButtonDefaults.ButtonSize.Medium, + border = HedvigTheme.colorScheme.borderPrimary, + ) + } Spacer(modifier = Modifier.height(16.dp)) - Column( modifier = Modifier.fillMaxWidth(), ) { @@ -157,32 +183,35 @@ private fun ManualChargeSuccessScreen(uiState: ManualChargeUiState.Success) { horizontalArrangement = Arrangement.SpaceBetween, ) { HedvigText( - text = "Due date", //todo - color = HedvigTheme.colorScheme.textSecondary, - style = HedvigTheme.typography.label - ) - HedvigText( - text = dateTimeFormatterWithYear.format(uiState.dueDate), - color = HedvigTheme.colorScheme.textSecondary, - style = HedvigTheme.typography.label - ) - } - Spacer( Modifier.height(10.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - HedvigText( - text = "Bank account", //todo + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_DUE_DATE), //todo color = HedvigTheme.colorScheme.textSecondary, - style = HedvigTheme.typography.label + style = HedvigTheme.typography.label, ) HedvigText( - text = "*** *3242", //todo + text = dateTimeFormatterWithYear.format(uiState.manualChargeInfo.missedDueDate), color = HedvigTheme.colorScheme.textSecondary, style = HedvigTheme.typography.label, ) } + if (uiState.manualChargeInfo.bankDescriptor!=null && + uiState.manualChargeInfo.bankAccountDisplayValue!=null) { + Spacer(Modifier.height(10.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + HedvigText( + text = uiState.manualChargeInfo.bankDescriptor, + color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label, + ) + HedvigText( + text = uiState.manualChargeInfo.bankAccountDisplayValue, + color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label, + ) + } + } } Spacer(modifier = Modifier.height(16.dp)) @@ -197,16 +226,16 @@ private fun ManualChargeSuccessScreen(uiState: ManualChargeUiState.Success) { verticalAlignment = Alignment.CenterVertically, ) { HedvigText( - text = "Total", + text = stringResource(Res.string.payment_details_receipt_card_total), ) HedvigText( - text = uiState.amount.toString(), + text = uiState.manualChargeInfo.amountDue.toString(), textAlign = TextAlign.End, ) } HedvigButton( - text = "Pay ${uiState.amount}", + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_PAY, uiState.manualChargeInfo.amountDue), onClick = { /* TODO: Handle payment */ }, enabled = true, modifier = Modifier.fillMaxWidth(), @@ -214,7 +243,7 @@ private fun ManualChargeSuccessScreen(uiState: ManualChargeUiState.Success) { Spacer(modifier = Modifier.height(8.dp)) HedvigText( - text = "Ensure your account has enough\nfunds to cover this payment", + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_FINE_PRINT), color = HedvigTheme.colorScheme.textSecondaryTranslucent, textAlign = TextAlign.Center, style = HedvigTheme.typography.label, @@ -234,11 +263,17 @@ private fun ManualChargeScreenSuccessPreview() { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { ManualChargeScreen( uiState = ManualChargeUiState.Success( - dueDate = LocalDate(2026, 1, 1), - amount = UiMoney(100.0, UiCurrencyCode.SEK), + ManualChargeInfo( + missedDueDate = LocalDate(2026, 1, 15), + amountDue = UiMoney(100.0, UiCurrencyCode.SEK), + chargeId = "chargeId", + bankDescriptor = "Bank account", + bankAccountDisplayValue = "**** 8324" + ) ), navigateUp = {}, reload = {}, + {} ) } } @@ -254,6 +289,7 @@ private fun ManualChargeScreenLoadingPreview() { uiState = ManualChargeUiState.Loading, navigateUp = {}, reload = {}, + {} ) } } @@ -269,6 +305,7 @@ private fun ManualChargeScreenFailurePreview() { uiState = ManualChargeUiState.Failure(ManualChargeFailureReason.GeneralFailure), navigateUp = {}, reload = {}, + {} ) } } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt index b926b434f3..263a124e26 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt @@ -1,20 +1,27 @@ package com.hedvig.android.feature.payments.ui.manualcharge import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.feature.payments.data.GetManualChargeInfoUseCase +import com.hedvig.android.feature.payments.data.ManualChargeInfo +import com.hedvig.android.feature.payments.data.TriggerManualChargeUseCase import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope import com.hedvig.android.molecule.public.MoleculeViewModel import kotlinx.datetime.LocalDate -internal class ManualChargeViewModel : MoleculeViewModel( +internal class ManualChargeViewModel( + getManualChargeInfoUseCase: GetManualChargeInfoUseCase, + triggerManualCharge: TriggerManualChargeUseCase +) : MoleculeViewModel( initialState = ManualChargeUiState.Loading, - presenter = ManualChargePresenter(), + presenter = ManualChargePresenter(getManualChargeInfoUseCase, triggerManualCharge), ) -private class ManualChargePresenter : MoleculePresenter { +private class ManualChargePresenter( + private val getManualChargeInfoUseCase: GetManualChargeInfoUseCase, + private val triggerManualCharge: TriggerManualChargeUseCase +) : MoleculePresenter { @Composable override fun MoleculePresenterScope.present( lastState: ManualChargeUiState, @@ -32,8 +39,7 @@ internal sealed interface ManualChargeUiState { ) : ManualChargeUiState data class Success( - val dueDate: LocalDate, - val amount: UiMoney + val manualChargeInfo: ManualChargeInfo ) : ManualChargeUiState } From a0312718becf9e53f0a72b9b19b9ebf8126238aa Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Tue, 28 Apr 2026 15:47:15 +0200 Subject: [PATCH 10/29] adjust to new api --- .../graphql/QueryManualChargeInfo.graphql | 11 -- .../main/graphql/QueryUpcomingPayment.graphql | 10 ++ .../data/GetManualChargeInfoUseCase.kt | 27 +--- .../feature/payments/di/PaymentsModule.kt | 1 - .../data/GetUpcomingPaymentUseCase.kt | 131 +++++++++--------- .../manualcharge/ManualChargeDestination.kt | 2 +- .../flags/UnleashFeatureFlagProvider.kt | 2 - .../android/featureflags/flags/Feature.kt | 1 - 8 files changed, 80 insertions(+), 105 deletions(-) diff --git a/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql b/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql index 5104fcf9e1..eebb50380b 100644 --- a/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql +++ b/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql @@ -1,15 +1,5 @@ query ManualChargeInfo { currentMember { - futureCharge { - ...on MemberCharge { - chargeBreakdown { - periods { - isPreviouslyFailedCharge - } - } - } - - } pastCharges { ...on MemberCharge { id @@ -20,7 +10,6 @@ query ManualChargeInfo { } } } - paymentInformation { status chargeMethod { diff --git a/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql b/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql index c6e01c8d77..af63d30a06 100644 --- a/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql +++ b/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql @@ -1,5 +1,15 @@ query UpcomingPayment { currentMember { + missedChargeIdToChargeManually + pastCharges { + ...on MemberCharge { + id + status + net { + ...MoneyFragment + } + } + } activeContracts { terminationDueToMissedPayments terminationDate diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt index 58c619f025..9d0151971c 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt @@ -26,42 +26,24 @@ internal interface GetManualChargeInfoUseCase { internal class GetManualChargeInfoUseCaseImpl( private val apolloClient: ApolloClient, - private val featureManager: FeatureManager, ): GetManualChargeInfoUseCase { override suspend fun invoke(): Either = either { - val isFeatureEnabled = featureManager.isFeatureEnabled(Feature.ENABLE_MANUAL_CHARGE).first() - if (!isFeatureEnabled) { - logcat {"GetManualChargeInfoUseCaseImpl: manual charge FF is off"} - raise(ErrorMessage()) - } - val currentMember = apolloClient.query(ManualChargeInfoQuery()) .fetchPolicy(NetworkFirst) .safeExecute(::ErrorMessage) .bind() .currentMember - val isPaymentMethodTrustly = currentMember.paymentInformation.status == MemberPaymentConnectionStatus.ACTIVE && - currentMember.paymentInformation.chargeMethod?.paymentMethod.toChargeMethod() == - MemberPaymentChargeMethod.TRUSTLY - - if (!isPaymentMethodTrustly) { - logcat {"GetManualChargeInfoUseCaseImpl: payment method not Trustly"} - raise(ErrorMessage()) - } + val showManualCharge = currentMember.missedChargeIdToChargeManually - val isFailedInUpcomingPayment = currentMember.futureCharge?.chargeBreakdown - ?.flatMap { it.periods } - ?.any { it.isPreviouslyFailedCharge } == true - if (!isFailedInUpcomingPayment) { - logcat {"GetManualChargeInfoUseCaseImpl: no failed in upcoming payment"} + if (showManualCharge==null) { + logcat {"GetManualChargeInfoUseCaseImpl: missedChargeIdToChargeManually is null"} raise(ErrorMessage()) } val latestFailedPastCharge = currentMember.pastCharges - .maxByOrNull { it.date } - .takeIf { it?.status == MemberChargeStatus.FAILED } + .firstOrNull {it.id == showManualCharge} if (latestFailedPastCharge==null) { logcat {"GetManualChargeInfoUseCaseImpl: latestFailedPastCharge is null"} @@ -76,7 +58,6 @@ internal class GetManualChargeInfoUseCaseImpl( bankDescriptor = currentMember.paymentInformation.chargeMethod?.descriptor ) } -//TODO: all these flags will be moved to BE } internal data class ManualChargeInfo( diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt index 5878915422..bcbddfeea4 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt @@ -132,7 +132,6 @@ val paymentsModule = module { single { GetManualChargeInfoUseCaseImpl( get(), - get(), ) } 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 7717756ef7..fe46b93b5d 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 @@ -22,17 +22,16 @@ import com.hedvig.android.feature.payments.data.PaymentOverview.OngoingCharge import com.hedvig.android.feature.payments.data.toChargeMethod import com.hedvig.android.feature.payments.data.toFailedCharge import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature import kotlin.time.Clock import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.isActive import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime @@ -51,76 +50,76 @@ internal data class GetUpcomingPaymentUseCaseImpl( val clock: Clock, ) : GetUpcomingPaymentUseCase { override suspend fun invoke(): Flow> { - return combine( - flow { - while (currentCoroutineContext().isActive) { - emitAll( - apolloClient.query(UpcomingPaymentQuery()) - .fetchPolicy(FetchPolicy.NetworkFirst) - .safeFlow(::ErrorMessage), - ) - delay(3.seconds) - } - }, - featureManager.isFeatureEnabled(Feature.ENABLE_MANUAL_CHARGE), - ) { response, isManualChargeFlagEnabled -> - either { - val result = response.bind() - val paymentConnection = run { - val paymentInformation = result.currentMember.paymentInformation - when (paymentInformation.status) { - MemberPaymentConnectionStatus.ACTIVE -> { - PaymentConnection.Active( - displayName = paymentInformation.chargeMethod?.displayName, - displayValue = paymentInformation.chargeMethod?.descriptor, - chargeMethod = paymentInformation.chargeMethod?.paymentMethod.toChargeMethod(), - ) - } + return flow { + while (currentCoroutineContext().isActive) { + emitAll( + apolloClient.query(UpcomingPaymentQuery()) + .fetchPolicy(FetchPolicy.NetworkFirst) + .safeFlow(::ErrorMessage) + .map { response -> - MemberPaymentConnectionStatus.PENDING -> { - PaymentConnection.Pending - } + either { + val result = response.bind() + val paymentConnection = run { + val paymentInformation = result.currentMember.paymentInformation + when (paymentInformation.status) { + MemberPaymentConnectionStatus.ACTIVE -> { + PaymentConnection.Active( + displayName = paymentInformation.chargeMethod?.displayName, + displayValue = paymentInformation.chargeMethod?.descriptor, + chargeMethod = paymentInformation.chargeMethod?.paymentMethod.toChargeMethod(), + ) + } - MemberPaymentConnectionStatus.NEEDS_SETUP -> { - val firstKnownTerminationDateForContractTerminatedDueToMissedPayments = result - .currentMember - .activeContracts - .filter { it.terminationDueToMissedPayments } - .mapNotNull { it.terminationDate } - .sorted() - .firstOrNull() - PaymentConnection.NeedsSetup(firstKnownTerminationDateForContractTerminatedDueToMissedPayments) - } + MemberPaymentConnectionStatus.PENDING -> { + PaymentConnection.Pending + } - MemberPaymentConnectionStatus.UNKNOWN__ -> { - PaymentConnection.Unknown - } - } - } - val memberChargeShortInfo = result.currentMember.futureCharge?.toMemberChargeShortInfo() + MemberPaymentConnectionStatus.NEEDS_SETUP -> { + val firstKnownTerminationDateForContractTerminatedDueToMissedPayments = result + .currentMember + .activeContracts + .filter { it.terminationDueToMissedPayments } + .mapNotNull { it.terminationDate } + .sorted() + .firstOrNull() + PaymentConnection.NeedsSetup(firstKnownTerminationDateForContractTerminatedDueToMissedPayments) + } - val isManualChargeAllowed = if ( - isManualChargeFlagEnabled && - paymentConnection is Active && - paymentConnection.chargeMethod == MemberPaymentChargeMethod.TRUSTLY && - memberChargeShortInfo?.failedCharge != null - ) { - ManualChargeToPrompt( - memberChargeShortInfo.failedCharge.sum, - ) - } else { - null - } + MemberPaymentConnectionStatus.UNKNOWN__ -> { + PaymentConnection.Unknown + } + } + } + val memberChargeShortInfo = result.currentMember.futureCharge?.toMemberChargeShortInfo() - PaymentOverview( - memberChargeShortInfo = memberChargeShortInfo, - ongoingCharges = result.currentMember.ongoingCharges.mapNotNull { - val id = it.id ?: return@mapNotNull null - OngoingCharge(id, it.date, UiMoney.fromMoneyFragment(it.net)) - }, - isManualChargeAllowed = isManualChargeAllowed, - paymentConnection = paymentConnection, + val missedChargeIdToChargeManually: String? = result.currentMember.missedChargeIdToChargeManually + + val isManualChargeAllowed = if (missedChargeIdToChargeManually!=null) { + val failedChargeNet = result.currentMember.pastCharges.firstOrNull { + it.id == missedChargeIdToChargeManually}?.net?.let { net -> + UiMoney.fromMoneyFragment(net) + } + if (failedChargeNet!=null) { + ManualChargeToPrompt(failedChargeNet) + } else null + } else { + null + } + + PaymentOverview( + memberChargeShortInfo = memberChargeShortInfo, + ongoingCharges = result.currentMember.ongoingCharges.mapNotNull { + val id = it.id ?: return@mapNotNull null + OngoingCharge(id, it.date, UiMoney.fromMoneyFragment(it.net)) + }, + isManualChargeAllowed = isManualChargeAllowed, + paymentConnection = paymentConnection, + ) + } + }, ) + delay(3.seconds) } } } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt index 5a506e7e01..22e31bd0b6 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt @@ -183,7 +183,7 @@ private fun ManualChargeSuccessScreen( horizontalArrangement = Arrangement.SpaceBetween, ) { HedvigText( - text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_DUE_DATE), //todo + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_DUE_DATE), color = HedvigTheme.colorScheme.textSecondary, style = HedvigTheme.typography.label, ) diff --git a/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt b/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt index 59aa1c36cf..0040e59b9a 100644 --- a/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt +++ b/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt @@ -37,8 +37,6 @@ internal class UnleashFeatureFlagProvider( Feature.DISABLE_REDEEM_CAMPAIGN -> hedvigUnleashClient.client.isEnabled("disable_redeem_campaign", false) Feature.ENABLE_CLAIM_HISTORY -> hedvigUnleashClient.client.isEnabled("enable_claim_history", false) - - Feature.ENABLE_MANUAL_CHARGE -> hedvigUnleashClient.client.isEnabled("enable_manual_charge") } }.distinctUntilChanged() } 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 f5747c4238..2db61ba0e0 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 @@ -22,5 +22,4 @@ enum class Feature( ), DISABLE_REDEEM_CAMPAIGN("Disables the ability to redeem a campaign code"), ENABLE_CLAIM_HISTORY("Enables claim history"), - ENABLE_MANUAL_CHARGE("Enables manual self-charge for member in the app payments") } From 3d0e3123e5bd7cab90ed2d3423eb4a24d4afeed1 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Tue, 28 Apr 2026 16:06:57 +0200 Subject: [PATCH 11/29] new schema --- .../android/apollo/octopus/schema.graphqls | 279 ++++++++++++++---- .../graphql/QueryManualChargeInfo.graphql | 1 + 2 files changed, 222 insertions(+), 58 deletions(-) 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 288983627b..99ac458a10 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 @@ -468,6 +468,41 @@ type BundleYearlySavings { """ bundleDiscountCoversFullPeriod: Boolean! } +""" +SHA-256-hashed user data for Facebook Conversions API (CAPI). +All non-null field values are hex-encoded SHA-256 hashes of the normalized plaintext. +Normalization follows https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters +""" +type CapiUserData { + """ + Hashed email (em) + """ + em: String! + """ + Hashed Gmail-normalized email, only present for Gmail addresses (gem) + """ + gem: String + """ + Hashed first name (fn) + """ + fn: String! + """ + Hashed last name (ln) + """ + ln: String! + """ + Hashed postal/zip code, null if unavailable (zp) + """ + zp: String + """ + Hashed city, null if unavailable (ct) + """ + ct: String + """ + Hashed country code, e.g. 'se' (co) + """ + co: String! +} type CarItemNotification { message: String! } @@ -1687,15 +1722,15 @@ returned as ExtendedItemDiscount """ type ExtendedItemDiscount { """ - General discount information + General discount information """ itemDiscount: ItemDiscount! """ - Monthly reduction applied by the discount. It's a negative number + Monthly reduction applied by the discount. It's a negative number """ amount: Money! """ - Whether discount is on a pending state or not + Whether discount is on a pending state or not """ isPending: Boolean! } @@ -1728,6 +1763,11 @@ type ExternalInsurer { displayName: String! insurelyId: String } +type FetchedExternalInsurance { + displayName: String! + subtitle: String + insurer: ExternalInsurer! +} type FirstVetAction { sections: [FirstVetSection!]! } @@ -2377,6 +2417,9 @@ type LinkInfo { type Location { street: String } +type ManuallyChargeMemberMutationOutput { + userError: UserError +} """ A 'Member' is the central user-like concept of our platform, referring to someone who has bought insurance with Hedvig and is now as we call is a "member". @@ -2409,6 +2452,11 @@ type Member { Payment information for this member. """ paymentInformation: MemberPaymentInformation! + """ + Null if the latest charge was successful or self manual charge is not allowed. + Id of the latest charge if it failed and self manual charge allowed. + """ + missedChargeIdToChargeManually: UUID paymentMethods: MemberPaymentMethods! conversations: [Conversation!]! legacyConversation: Conversation @@ -2419,6 +2467,8 @@ type Member { claims: [Claim!]! claimsActive: [Claim!]! claimsHistory: [Claim!]! + partnerClaimsActive: [PartnerClaim!]! + partnerClaimsHistory: [PartnerClaim!]! firstName: String! lastName: String! ssn: String @@ -2477,6 +2527,11 @@ type Member { """ crossSellV2(input: CrossSellInput!): CrossSellV2! """ + Young Pet Guide stories for the member. + Returns a list of educational content stories for young pet owners. + """ + puppyGuideStories: [PuppyGuideStory!]! + """ Fetch all the active contracts for this member. Active contracts include all insurances that are either active today, or to-be-active in the future. """ @@ -2729,6 +2784,22 @@ type MemberPaymentAvailablePaymentMethod { True if the member can set up this payment method for payout. """ supportsPayout: Boolean! + """ + True if this method is already ACTIVE for member and can be chosen as default directly without setup, false if + this is a new payment method that the member has not yet set up. + If this is true, then the `details` field will be populated with the payment method details. If this is false, then + the `details` field will be null since the member has not yet set up this payment method. + If true then this method can be set up as default directly by calling `paymentMethodSetDefaultPayin` or + `paymentMethodSetDefaultPayout` mutation depending on if it's a payin or payout method. If false, then the + corresponding mutation for setting up this payment method should be called, eg. `paymentMethodSetupTrustly`, + `paymentMethodSetupSwishPayin` etc. + """ + isActive: Boolean! + """ + For already connected and ACTIVE methods, ie isActive=true, specific details of the actual connection - e.g. a bank + account reference, phone number for swish, or email/kivra for invoice. + """ + details: PaymentMethodDetails } type MemberPaymentChargeMethodInfo { """ @@ -2801,48 +2872,76 @@ type MemberPaymentInformation { } type MemberPaymentMethod { """ - The unique id of the payment method. This id is used for switching default and revoking payment methods. - """ - id: ID! - """ - Payment provider, eg Trustly, Swish, Nordea, Kivra etc. + Payment provider, eg Trustly, Swish, Nordea, Kivra etc. + This is used as the "identifier" of the payment method since there can only be one ACTIVE or PENDING payment method + per provider. """ provider: MemberPaymentProvider! """ - The payment method status - ACTIVE, PENDING, or PENDING_DEFAULT. - PENDING_DEFAULT means the payment method is awaiting activation and will become default once activated. + The payment method status - ACTIVE, PENDING. + If ACTIVE, the payment method is ready to use for payins or payouts depending on if it's a payin or payout method. + If PENDING, the payment method has been set up but is still awaiting activation and cannot be used for payins or + payouts until then. Once activated, the status will change to ACTIVE. """ status: MemberPaymentMethodStatus! """ - True if this is the default payment method. Only one ACTIVE payment method can be default at a time. - If status is PENDING then payment method will become the default once activated. + This is 'true' for only one of the members ACTIVE methods which is the default payment method that will be used for + charging or payout the member. For PENDING methods, this can also be 'true' if the member has chosen to set up this + payment method as default during the setup process. """ isDefault: Boolean! """ - Specific details of the actual connection - e.g. a bank account reference, phone number for swish, - or email/kivra for invoice. + Specific details of the actual connection if method is ACTIVE - e.g. a bank account reference, phone number for swish, + or email/kivra for invoice. If method is PENDING, then this field will be null since the member has not yet set up + this payment method. """ - details: PaymentMethodDetails! + details: PaymentMethodDetails } type MemberPaymentMethods { """ - List of active and pending payment payin methods for this member. + List of all member's ACTIVE and PENDING payment payin methods. + A member can have multiple ACTIVE payment methods with these constraints: + - Only one ACTIVE payment method per provider, eg. one Trustly, one Swish, one Nordea etc. + - Only one ACTIVE payment method can be default at a time. + A member can have multiple PENDING payment methods with these constraints: + - Only one PENDING payment method per provider, eg. one Trustly, one Swish, one Nordea etc. + So there can exist max two payment methods per provider, one ACTIVE and one PENDING. + If a PENDING payment method has isDefault=true, then it will become the default ACTIVE payment method once activated. """ payinMethods: [MemberPaymentMethod!]! """ - List of active and pending payment payout methods for this member. + List of all member's ACTIVE and PENDING payment payout methods. + A member can have multiple ACTIVE payment methods with these constraints: + - Only one ACTIVE payment method per provider, eg. one Trustly, one Swish, one Nordea etc. + - Only one ACTIVE payment method can be default at a time. + A member can have multiple PENDING payment methods with these constraints: + - Only one PENDING payment method per provider, eg. one Trustly, one Swish, one Nordea etc. + So there can exist max two payment methods per provider, one ACTIVE and one PENDING. + If a PENDING payment method has isDefault=true, then it will become the default ACTIVE payment method once activated. """ payoutMethods: [MemberPaymentMethod!]! """ - The default payment method for payin if any. + The default payment method to use for payins if any. + Note that there can exist a PENDING payment method in `payinMethods` list with `isDefault`=true, in that case this default + payment method will be replaced by it once the pending method is activated. """ defaultPayinMethod: MemberPaymentMethod """ - The default payment method for payout if any. + The default payment method to use for payouts if any. + Note that there can exist a PENDING payment method in `payoutMethods` list with `isDefault`=true, in that case this default + payment method will be replaced by it once the pending method is activated. """ defaultPayoutMethod: MemberPaymentMethod """ - The available payment methods that the member can choose from when setting up a new payment method. + The available payment methods that the member can choose from when setting up a new payment method. + This list can include both payment methods that the member has already set up and new payment methods that the + member has not yet set up but are available to them. For already set up payment methods, the `isActive` field will + be true and the `details` field will be populated with the payment method details. For new payment methods that the + member has not yet set up, the `isActive` field will be false and the `details` field will be null. + If member picks a new payment method to set up, the corresponding mutation for setting up that payment method should + be called, eg. `paymentMethodSetupTrustly`, `paymentMethodSetupSwishPayin` etc. + If member picks an already set up payment method to set up as default, then `paymentMethodSetDefaultPayin` or + `paymentMethodSetDefaultPayout` mutation should be called depending on if it's a payin or payout method. """ availableMethods: [MemberPaymentAvailablePaymentMethod!]! """ @@ -3389,12 +3488,13 @@ input MoveToHouseInput { } type Mutation { registerDirectDebit2(clientContext: RegisterDirectDebitClientContext2): DirectDebitResponse2! + manuallyChargeMember(dueDate: Date!): ManuallyChargeMemberMutationOutput! """ Setup invoice payment method for the member. Kivra will be used as the provider if supported, else mail. """ - paymentMethodSetupInvoicePayin(input: PaymentMethodSetupInvoicePayinInput!): PaymentMethodSetupOutput! + paymentMethodSetupInvoicePayin: PaymentMethodSetupOutput! """ - Setup Trustly payment payin and payout method for the member. + Setup Trustly payment payin and payout method for the member. Requires member consent via redirect to Trustly URL in response. """ paymentMethodSetupTrustly(input: PaymentMethodSetupTrustlyInput!): PaymentMethodSetupOutput! """ @@ -3406,17 +3506,19 @@ type Mutation { """ paymentMethodSetupSwishPayout(input: PaymentMethodSetupSwishInput!): PaymentMethodSetupOutput! """ - Setup Swish payin method for the member. + Setup Swish payin method for the member. Requires member consent in Swish app. """ paymentMethodSetupSwishPayin(input: PaymentMethodSetupSwishInput!): PaymentMethodSetupOutput! """ - Revoke an active payment method. The member will be required to set up a new payment method if they revoke their default one. + A member can have multiple ACTIVE payment methods where one of those is default. This mutation changes the + members default payment method for charging to any of his/hers other active payment methods. """ - paymentMethodRevoke(id: ID!): UserError + paymentMethodSetDefaultPayin(provider: MemberPaymentProvider!): UserError """ - Set an active payment method as default. + A member can have multiple ACTIVE payment methods where one of those is default. This mutation changes the + members default payment method for payouts to any of his/hers other active payment methods. """ - paymentMethodSetDefault(id: ID!): UserError + paymentMethodSetDefaultPayout(provider: MemberPaymentProvider!): UserError """ Start a conversation. This is effectively creating one, but with two slight differences from a regular "create something"-mutation: @@ -3545,7 +3647,7 @@ type Mutation { Update the raw insurance-related data for this `PriceIntent`. This data is mostly related to the insured object itself, and not the "holder" of the insurance. """ - priceIntentDataUpdate(priceIntentId: UUID!, data: PricingFormData!): PriceIntentMutationOutput! + priceIntentDataUpdate(priceIntentId: UUID!, data: PricingFormData!, applySuggestedData: Boolean): PriceIntentMutationOutput! """ Associate a specific Insurely `dataCollectionId` from lookup-service with this PriceIntent. """ @@ -3576,6 +3678,10 @@ type Mutation { """ productOfferReprice(offerId: UUID!, data: PricingFormData!): ProductOffersMutationOutput! """ + Mark a young pet guide story as read for a specific member. + """ + puppyGuideEngagement(engagement: PuppyEngagementInput!): PuppyGuideStoryMutationOutput! + """ Update the customer of the shop session. Only non-null fields will be changed. Can trigger automatic lookup of other information. The session can be placed in a "point of no return" state where it is no longer legal to update the customer, @@ -3676,6 +3782,22 @@ type Mutation { """ upsellTravelAddonActivate(quoteId: ID!, addonId: ID!): UpsellTravelAddonActivationOutput! } +type PartnerClaim { + id: ID! + externalId: String! + exposureDisplayName: String + status: ClaimStatus + submittedAt: Date + payoutAmount: Money + associatedTypeOfContract: String + claimType: String + handlerEmail: String + displayItems: [ClaimDisplayItem!]! + """ + Terms & conditions for the claim found using claims contractId and dateOfOccurrence, otherwise null. + """ + productVariant: ProductVariant +} type PartnerData { sas: SasPartnerData } @@ -3684,7 +3806,7 @@ type PartnerWidgetTrial { } type PaymentMethodBankAccountDetails { """ - The bank account reference - e.g. clearing number and account number. + The bank account reference - e.g. clearing number + account number. """ account: String! """ @@ -3707,21 +3829,7 @@ type PaymentMethodInvoiceDetails { """ email: String } -input PaymentMethodSetupInvoicePayinInput { - """ - Set up invoice payment method as default. - """ - setAsDefaultPayout: Boolean! -} input PaymentMethodSetupNordeaPayoutInput { - """ - Set up Nordea payout method as default. - """ - setAsDefault: Boolean! - """ - The clearing number for member's bank account. - """ - clearingNumber: String! """ The account number for member's bank account. """ @@ -3732,6 +3840,10 @@ type PaymentMethodSetupOutput { The status of the setup process. If FAILED the reason for failure can be found in the `error` field. """ status: PaymentMethodSetupStatus! + """ + The order id for the payment method setup order if SUCCESSFUL. + """ + orderId: ID """ Url to redirect the member to if any. """ @@ -3756,24 +3868,12 @@ enum PaymentMethodSetupStatus { FAILED } input PaymentMethodSetupSwishInput { - """ - Set up Swish payment method as default. - """ - setAsDefault: Boolean! """ The Swish mobile number to use for payout or payin. """ phoneNumber: String! } input PaymentMethodSetupTrustlyInput { - """ - Set up Trustly payment method as default for payin. - """ - setAsDefaultPayin: Boolean! - """ - Set up Trustly payment method as default for payout. - """ - setAsDefaultPayout: Boolean! """ The URL to redirect the member back to after a successful setup after Trustly onboarding. """ @@ -3873,11 +3973,11 @@ type PriceIntent { """ product: Product! """ - Submitted user form data. + UI-safe masked form data. PII fields (street, zipCode, city) are always masked. """ data: PricingFormData! """ - Data submitted in other places or inferred from other data points + Masked, uncommitted form data from automatic lookup (e.g. SPAR address, trial data). PII fields are masked the same way as 'data'. """ suggestedData: PricingFormData! """ @@ -3905,6 +4005,15 @@ type PriceIntent { When 'true' it means user has gone trough Insurely flow with that price intent """ hasCollectedInsurelyData: Boolean! + """ + List of external insurances fetched via Insurely that correspond to products Hedvig offers. + Null when no Insurely data collection has been associated with this price intent. + """ + fetchedExternalInsurances: [FetchedExternalInsurance!] + """ + When 'true' all required form data has been provided and the price intent can be confirmed. + """ + isReadyToConfirm: Boolean! } enum PriceIntentAnimal { CAT @@ -4065,7 +4174,7 @@ type ProductOffer { """ priceIntentId: UUID """ - The form data used to generate the offer + UI-safe masked form data used to generate the offer. PII fields (street, zipCode, city) are masked when address came from registration address lookup. """ priceIntentData: PricingFormData! """ @@ -4299,6 +4408,53 @@ type ProductVariantComparisonRow { """ covered: [String!]! } +input PuppyEngagementInput { + name: String! + rating: Int + opened: Boolean + read: Boolean + closed: Boolean +} +type PuppyGuideStory { + """ + The unique name/identifier of the story. + """ + name: String! + """ + The display title of the story. + """ + title: String! + """ + The subtitle or description of the story. + """ + subtitle: String! + """ + The main content of the story. + """ + content: String! + """ + The image associated with this story. + """ + image: String! + """ + Categories this story belongs to. + """ + categories: [String!]! + """ + The date when the story was marked as read by the user. + """ + read: Boolean! + """ + The user's rating of the story. + """ + rating: Int +} +type PuppyGuideStoryMutationOutput { + """ + Indicates whether the mutation was successful. + """ + success: Boolean! +} type Query { """ Return a conversation for a given ID. @@ -4306,6 +4462,7 @@ type Query { """ conversation(id: UUID!): Conversation claim(id: ID!): Claim + partnerClaim(id: ID!): PartnerClaim claimIntent(id: ID!): ClaimIntent! claimIntentFormFieldSearch(input: ClaimIntentFormFieldSearchInput!): ClaimIntentFormFieldSearchOutput! personalInformation(input: PersonalInformationInput!): PersonalInformation @@ -4614,6 +4771,12 @@ type ShopSessionOutcome { Note that this will not contain and `PendingContract`s. """ createdContracts: [Contract!]! + """ + Pre-hashed Facebook CAPI user data (SHA-256 hex, lowercase) for this signed session. + Fields follow Meta's Conversions API customer information parameter spec. + Requires authentication (inherits MemberAccessible from the outcome). + """ + capiUserData: CapiUserData! } type ShopSessionSigning { id: UUID! diff --git a/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql b/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql index eebb50380b..1cbc3a7bb6 100644 --- a/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql +++ b/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql @@ -1,5 +1,6 @@ query ManualChargeInfo { currentMember { + missedChargeIdToChargeManually pastCharges { ...on MemberCharge { id From 157ecf25fbfab85f8152ac1313b9b6ab8eaa1fd3 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 29 Apr 2026 00:53:24 +0200 Subject: [PATCH 12/29] impl red dot wip --- .../com/hedvig/android/app/MainActivity.kt | 3 ++ .../com/hedvig/android/app/ui/HedvigApp.kt | 3 ++ .../hedvig/android/app/ui/HedvigAppState.kt | 14 +++++++ .../com/hedvig/android/app/ui/HedvigAppUi.kt | 5 +++ .../hedvig/android/app/ui/NavigationSuite.kt | 3 ++ .../design/system/hedvig/NavigationBar.kt | 9 ++++- .../main/graphql/QueryMissedPayment.graphql | 5 +++ .../badge/data/di/NotificationBadgeModule.kt | 19 ++++++++++ .../data/payment/GetMissedPaymentIdUseCase.kt | 38 +++++++++++++++++++ .../MissedPaymentNotificationService.kt | 32 ++++++++++++++++ 10 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 app/notification-badge-data/notification-badge-data-public/src/main/graphql/QueryMissedPayment.graphql create mode 100644 app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetMissedPaymentIdUseCase.kt create mode 100644 app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/MissedPaymentNotificationService.kt 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 6eab5f8368..1397dff029 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 @@ -47,6 +47,7 @@ import com.hedvig.android.logger.LogPriority import com.hedvig.android.logger.logcat import com.hedvig.android.navigation.core.HedvigDeepLinkContainer import com.hedvig.android.navigation.core.allDeepLinkUriPatterns +import com.hedvig.android.notification.badge.data.payment.MissedPaymentNotificationServiceProvider import com.hedvig.android.theme.Theme import com.stylianosgakis.navigation.recents.url.sharing.provideAssistContent import java.util.Locale @@ -74,6 +75,7 @@ class MainActivity : AppCompatActivity() { private val logoutUseCase: LogoutUseCase by inject() private val getMemberAuthorizationCodeUseCase: GetMemberAuthorizationCodeUseCase by inject() + private val missedPaymentNotificationServiceProvider: MissedPaymentNotificationServiceProvider by inject() private var navController: NavController? = null @@ -164,6 +166,7 @@ class MainActivity : AppCompatActivity() { externalNavigator = externalNavigator, logoutUseCase = logoutUseCase, getMemberAuthorizationCodeUseCase = getMemberAuthorizationCodeUseCase, + missedPaymentNotificationServiceProvider = missedPaymentNotificationServiceProvider, ) } } 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 7e710757f0..fc140e9a50 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 @@ -39,6 +39,7 @@ 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.feature.cross.sell.sheet.CrossSellSheet +import com.hedvig.android.notification.badge.data.payment.MissedPaymentNotificationServiceProvider import com.hedvig.android.feature.login.navigation.LoginDestination import com.hedvig.android.featureflags.FeatureManager import com.hedvig.android.language.LanguageService @@ -84,6 +85,7 @@ internal fun HedvigApp( externalNavigator: ExternalNavigator, logoutUseCase: LogoutUseCase, getMemberAuthorizationCodeUseCase: GetMemberAuthorizationCodeUseCase, + missedPaymentNotificationServiceProvider: MissedPaymentNotificationServiceProvider, ) { val hedvigAppState = rememberHedvigAppState( windowSizeClass = windowSizeClass, @@ -91,6 +93,7 @@ internal fun HedvigApp( getOnlyHasNonPayingContractsUseCase = getOnlyHasNonPayingContractsUseCase, featureManager = featureManager, navHostController = navHostController, + missedPaymentNotificationServiceProvider = missedPaymentNotificationServiceProvider, ) val darkTheme = hedvigAppState.darkTheme HedvigTheme(darkTheme = darkTheme) { 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 843328d3c8..900a6fb215 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 @@ -25,6 +25,7 @@ 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.feature.forever.navigation.ForeverDestination +import com.hedvig.android.notification.badge.data.payment.MissedPaymentNotificationServiceProvider import com.hedvig.android.feature.help.center.navigation.helpCenterCrossSellBottomSheetPermittingDestinations import com.hedvig.android.feature.home.home.navigation.HomeDestination import com.hedvig.android.feature.home.home.navigation.homeCrossSellBottomSheetPermittingDestinations @@ -62,6 +63,7 @@ internal fun rememberHedvigAppState( getOnlyHasNonPayingContractsUseCase: Provider, featureManager: FeatureManager, navHostController: NavHostController, + missedPaymentNotificationServiceProvider: MissedPaymentNotificationServiceProvider, coroutineScope: CoroutineScope = rememberCoroutineScope(), ): HedvigAppState { NavigationViewTrackingEffect(navController = navHostController) @@ -73,6 +75,7 @@ internal fun rememberHedvigAppState( settingsDataStore, getOnlyHasNonPayingContractsUseCase, featureManager, + missedPaymentNotificationServiceProvider, ) { HedvigAppState( navController = navHostController, @@ -81,6 +84,7 @@ internal fun rememberHedvigAppState( settingsDataStore = settingsDataStore, getOnlyHasNonPayingContractsUseCase = getOnlyHasNonPayingContractsUseCase, featureManager = featureManager, + missedPaymentNotificationServiceProvider = missedPaymentNotificationServiceProvider, ) } } @@ -93,6 +97,7 @@ internal class HedvigAppState( private val settingsDataStore: SettingsDataStore, getOnlyHasNonPayingContractsUseCase: Provider, featureManager: FeatureManager, + missedPaymentNotificationServiceProvider: MissedPaymentNotificationServiceProvider, ) { val currentDestination: NavDestination? @Composable get() = navController.currentBackStackEntryAsState().value?.destination @@ -162,6 +167,15 @@ internal class HedvigAppState( ), ) + val showPaymentsBadge: StateFlow = missedPaymentNotificationServiceProvider + .prodImpl + .showRedDotNotification() + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(5_000), + false, + ) + /** * UI logic for navigating to a top level destination in the app. Top level destinations have * only one copy of the destination of the back stack, and save and restore state whenever you diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigAppUi.kt b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigAppUi.kt index f9514c3e01..b2f395c3e8 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigAppUi.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigAppUi.kt @@ -44,6 +44,7 @@ import com.hedvig.android.design.system.hedvig.tokens.MotionTokens import com.hedvig.android.language.LanguageService import com.hedvig.android.navigation.activity.ExternalNavigator import com.hedvig.android.navigation.core.HedvigDeepLinkContainer +import com.hedvig.android.navigation.core.TopLevelGraph import hedvig.resources.EXIT_DEMO_MODE_BUTTON import hedvig.resources.Res import org.jetbrains.compose.resources.stringResource @@ -65,6 +66,7 @@ internal fun HedvigAppUi( logoutUseCase: LogoutUseCase, ) { val isDemoMode by demoManager.isDemoMode().collectAsState(false) + val showPaymentsBadge by hedvigAppState.showPaymentsBadge.collectAsState() val globalSnackBarState = rememberGlobalSnackBarState() Box(Modifier.fillMaxSize()) { Surface( @@ -76,6 +78,9 @@ internal fun HedvigAppUi( topLevelGraphs = hedvigAppState.topLevelGraphs.collectAsState().value, currentDestination = hedvigAppState.currentDestination, onNavigateToTopLevelGraph = hedvigAppState::navigateToTopLevelGraph, + getShowNotificationBadge = { graph -> + if (graph == TopLevelGraph.Payments) showPaymentsBadge else false + }, ) { Box( propagateMinConstraints = true, diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/ui/NavigationSuite.kt b/app/app/src/main/kotlin/com/hedvig/android/app/ui/NavigationSuite.kt index c99c32aa89..070b3e2f6d 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/ui/NavigationSuite.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/ui/NavigationSuite.kt @@ -30,6 +30,7 @@ internal fun NavigationSuite( currentDestination: NavDestination?, onNavigateToTopLevelGraph: (TopLevelGraph) -> Unit, modifier: Modifier = Modifier, + getShowNotificationBadge: (TopLevelGraph) -> Boolean = { false }, content: @Composable RowScope.() -> Unit, ) { Column(modifier) { @@ -49,6 +50,7 @@ internal fun NavigationSuite( onNavigateToDestination = onNavigateToTopLevelGraph, getIsCurrentlySelected = currentDestination::isTopLevelGraphInHierarchy, isExtraTall = navigationSuiteType == NavigationSuiteType.NavigationRailXLarge, + getShowNotificationBadge = getShowNotificationBadge, ) } content() @@ -62,6 +64,7 @@ internal fun NavigationSuite( destinations = topLevelGraphs, onNavigateToDestination = onNavigateToTopLevelGraph, getIsCurrentlySelected = currentDestination::isTopLevelGraphInHierarchy, + getShowNotificationBadge = getShowNotificationBadge, ) } } diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/NavigationBar.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/NavigationBar.kt index 0f69b77270..34bf71df73 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/NavigationBar.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/NavigationBar.kt @@ -77,6 +77,7 @@ fun NavigationBar( onNavigateToDestination: (TopLevelGraph) -> Unit, getIsCurrentlySelected: (TopLevelGraph) -> Boolean, modifier: Modifier = Modifier, + getShowNotificationBadge: (TopLevelGraph) -> Boolean = { false }, ) { val borderColor = NavigationTokens.BorderColor.value NavigationContainer(modifier) { @@ -110,6 +111,7 @@ fun NavigationBar( top = NavigationBarTokens.ItemTopPadding, bottom = NavigationBarTokens.ItemBottomPadding, ), + showNotificationBadge = getShowNotificationBadge(destination), modifier = Modifier.weight(1f) .semantics { role = Role.Tab @@ -128,6 +130,7 @@ fun NavigationRail( getIsCurrentlySelected: (TopLevelGraph) -> Boolean, isExtraTall: Boolean, modifier: Modifier = Modifier, + getShowNotificationBadge: (TopLevelGraph) -> Boolean = { false }, ) { val borderColor = NavigationTokens.BorderColor.value NavigationContainer(modifier.fillMaxHeight()) { @@ -177,6 +180,7 @@ fun NavigationRail( top = NavigationRailTokens.ItemTopPadding, bottom = NavigationRailTokens.ItemBottomPadding, ), + showNotificationBadge = getShowNotificationBadge(destination), modifier = Modifier.semantics { role = Role.Tab this.selected = selected @@ -214,6 +218,7 @@ private fun NavigationItem( onClick: () -> Unit, itemPaddings: PaddingValues, modifier: Modifier = Modifier, + showNotificationBadge: Boolean = false, ) { val interactionSource = remember { MutableInteractionSource() } var itemWidthPx by remember { mutableIntStateOf(0) } @@ -236,7 +241,9 @@ private fun NavigationItem( .padding(itemPaddings), horizontalAlignment = Alignment.CenterHorizontally, ) { - Box { + Box( + modifier = Modifier.notificationCircle(showNotificationBadge), + ) { Icon( imageVector = icon, contentDescription = EmptyContentDescription, diff --git a/app/notification-badge-data/notification-badge-data-public/src/main/graphql/QueryMissedPayment.graphql b/app/notification-badge-data/notification-badge-data-public/src/main/graphql/QueryMissedPayment.graphql new file mode 100644 index 0000000000..55500cd650 --- /dev/null +++ b/app/notification-badge-data/notification-badge-data-public/src/main/graphql/QueryMissedPayment.graphql @@ -0,0 +1,5 @@ +query MissedPayment { + currentMember { + missedChargeIdToChargeManually + } +} diff --git a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/di/NotificationBadgeModule.kt b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/di/NotificationBadgeModule.kt index 929a5eb692..e9528a0a62 100644 --- a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/di/NotificationBadgeModule.kt +++ b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/di/NotificationBadgeModule.kt @@ -10,6 +10,11 @@ import com.hedvig.android.notification.badge.data.crosssell.GetCrossSellRecommen import com.hedvig.android.notification.badge.data.crosssell.home.CrossSellHomeNotificationServiceImpl import com.hedvig.android.notification.badge.data.crosssell.home.CrossSellHomeNotificationServiceProvider import com.hedvig.android.notification.badge.data.crosssell.home.DemoCrossSellHomeNotificationService +import com.hedvig.android.notification.badge.data.payment.DemoMissedPaymentNotificationService +import com.hedvig.android.notification.badge.data.payment.GetMissedPaymentIdUseCase +import com.hedvig.android.notification.badge.data.payment.GetMissedPaymentIdUseCaseImpl +import com.hedvig.android.notification.badge.data.payment.MissedPaymentNotificationServiceImpl +import com.hedvig.android.notification.badge.data.payment.MissedPaymentNotificationServiceProvider import com.hedvig.android.notification.badge.data.storage.DatastoreNotificationBadgeStorage import com.hedvig.android.notification.badge.data.storage.NotificationBadgeStorage import org.koin.dsl.module @@ -33,4 +38,18 @@ val notificationBadgeModule = module { single { CrossSellHomeNotificationServiceImpl(get(), get>()) } + + single { + MissedPaymentNotificationServiceProvider( + demoManager = get(), + demoImpl = DemoMissedPaymentNotificationService(), + prodImpl = get(), + ) + } + single { + GetMissedPaymentIdUseCaseImpl(get()) + } + single { + MissedPaymentNotificationServiceImpl(get()) + } } diff --git a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetMissedPaymentIdUseCase.kt b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetMissedPaymentIdUseCase.kt new file mode 100644 index 0000000000..2ddc921178 --- /dev/null +++ b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetMissedPaymentIdUseCase.kt @@ -0,0 +1,38 @@ +package com.hedvig.android.notification.badge.data.payment + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.cache.normalized.FetchPolicy +import com.apollographql.apollo.cache.normalized.fetchPolicy +import com.hedvig.android.apollo.safeFlow +import com.hedvig.android.logger.logcat +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import octopus.MissedPaymentQuery + +interface GetMissedPaymentIdUseCase { + fun invoke(): Flow +} + +internal class GetMissedPaymentIdUseCaseImpl( + private val apolloClient: ApolloClient, +) : GetMissedPaymentIdUseCase { + override fun invoke(): Flow { + return apolloClient + .query(MissedPaymentQuery()) + .fetchPolicy(FetchPolicy.NetworkOnly) + .safeFlow() + .map { result -> + result.fold( + { + logcat(operationError = it) { + "Error when loading missed payment: $it" + } + false + }, + { data -> + data.currentMember.missedChargeIdToChargeManually != null + }, + ) + } + } +} diff --git a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/MissedPaymentNotificationService.kt b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/MissedPaymentNotificationService.kt new file mode 100644 index 0000000000..8d33c7473c --- /dev/null +++ b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/MissedPaymentNotificationService.kt @@ -0,0 +1,32 @@ +package com.hedvig.android.notification.badge.data.payment + +import com.hedvig.android.core.demomode.DemoManager +import com.hedvig.android.core.demomode.ProdOrDemoProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class MissedPaymentNotificationServiceProvider( + override val demoManager: DemoManager, + override val demoImpl: MissedPaymentNotificationService, + override val prodImpl: MissedPaymentNotificationService, +) : ProdOrDemoProvider + +interface MissedPaymentNotificationService { + fun showRedDotNotification(): Flow +} + +internal class DemoMissedPaymentNotificationService : MissedPaymentNotificationService { + var showNotification = false + + override fun showRedDotNotification(): Flow { + return flowOf(showNotification) + } +} + +internal class MissedPaymentNotificationServiceImpl( + private val getMissedPaymentIdUseCase: GetMissedPaymentIdUseCase, +) : MissedPaymentNotificationService { + override fun showRedDotNotification(): Flow { + return getMissedPaymentIdUseCase.invoke() + } +} From 9c6c90b3ca982288aa1ed0bf3aa9ef352da15003 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 29 Apr 2026 14:49:32 +0200 Subject: [PATCH 13/29] impl red dot; change api; success --- .../android/apollo/octopus/schema.graphqls | 99 ++++++++----------- .../androidMain/res/values-sv-rSE/strings.xml | 5 +- .../src/androidMain/res/values/strings.xml | 3 +- .../values-sv-rSE/strings.xml | 5 +- .../composeResources/values/strings.xml | 3 +- .../MutationManuallyChargeMember.graphql | 7 ++ .../data/TriggerManualChargeUseCase.kt | 29 +++++- .../feature/payments/di/PaymentsModule.kt | 2 +- .../navigation/PaymentsDestination.kt | 3 + .../payments/navigation/PaymentsGraph.kt | 15 ++- .../data/GetUpcomingPaymentUseCase.kt | 7 +- .../manualcharge/ManualChargeDestination.kt | 64 +++++++++--- .../ManualChargeSuccessDestination.kt | 69 +++++++++++++ .../ui/manualcharge/ManualChargeViewModel.kt | 66 ++++++++++--- .../badge/data/di/NotificationBadgeModule.kt | 10 +- .../data/payment/GetIfMissedPaymentUseCase.kt | 53 ++++++++++ .../data/payment/GetMissedPaymentIdUseCase.kt | 38 ------- .../MissedPaymentNotificationService.kt | 4 +- 18 files changed, 339 insertions(+), 143 deletions(-) create mode 100644 app/feature/feature-payments/src/main/graphql/MutationManuallyChargeMember.graphql create mode 100644 app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt create mode 100644 app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetIfMissedPaymentUseCase.kt delete mode 100644 app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetMissedPaymentIdUseCase.kt 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 99ac458a10..a925c8226d 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 @@ -2527,11 +2527,6 @@ type Member { """ crossSellV2(input: CrossSellInput!): CrossSellV2! """ - Young Pet Guide stories for the member. - Returns a list of educational content stories for young pet owners. - """ - puppyGuideStories: [PuppyGuideStory!]! - """ Fetch all the active contracts for this member. Active contracts include all insurances that are either active today, or to-be-active in the future. """ @@ -3655,8 +3650,10 @@ type Mutation { """ Confirm this PriceIntent, which will use the current data (and likely `ShopSession.customer`) to generate `ProductOffers` that can be added to the cart. + 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!): PriceIntentMutationOutput! + priceIntentConfirm(priceIntentId: UUID!, attribution: PriceIntentConfirmAttributionInput): 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. @@ -3678,10 +3675,6 @@ type Mutation { """ productOfferReprice(offerId: UUID!, data: PricingFormData!): ProductOffersMutationOutput! """ - Mark a young pet guide story as read for a specific member. - """ - puppyGuideEngagement(engagement: PuppyEngagementInput!): PuppyGuideStoryMutationOutput! - """ Update the customer of the shop session. Only non-null fields will be changed. Can trigger automatic lookup of other information. The session can be placed in a "point of no return" state where it is no longer legal to update the customer, @@ -4024,6 +4017,14 @@ type PriceIntentAnimalBreed { displayName: String! isMixedBreed: Boolean! } +input PriceIntentConfirmAttributionInput { + attributedTo: String + initiatedFrom: String + userFlow: String + flowSource: String + experiments: [ShopSessionExperimentInput!] + trackingData: JSON +} input PriceIntentCreateInput { shopSessionId: UUID! """ @@ -4343,6 +4344,11 @@ if the user has input enough information to generate it. type ProductRecommendation { product: Product! offer: ProductOffer + """ + External insurance data from Insurely, available even when no offer could be generated. + Null when no Insurely data collection is associated with the session. + """ + externalInsurance: RecommendationExternalInsurance } type ProductVariant { """ @@ -4408,53 +4414,6 @@ type ProductVariantComparisonRow { """ covered: [String!]! } -input PuppyEngagementInput { - name: String! - rating: Int - opened: Boolean - read: Boolean - closed: Boolean -} -type PuppyGuideStory { - """ - The unique name/identifier of the story. - """ - name: String! - """ - The display title of the story. - """ - title: String! - """ - The subtitle or description of the story. - """ - subtitle: String! - """ - The main content of the story. - """ - content: String! - """ - The image associated with this story. - """ - image: String! - """ - Categories this story belongs to. - """ - categories: [String!]! - """ - The date when the story was marked as read by the user. - """ - read: Boolean! - """ - The user's rating of the story. - """ - rating: Int -} -type PuppyGuideStoryMutationOutput { - """ - Indicates whether the mutation was successful. - """ - success: Boolean! -} type Query { """ Return a conversation for a given ID. @@ -4523,6 +4482,32 @@ type Query { """ addonOfferCost(quoteId: ID!, selectedAddonIds: [ID!]!): ItemCost! } +type RecommendationExternalInsurance { + """ + Display name of the external insurance product + """ + displayName: String! + """ + The external insurer + """ + insurer: ExternalInsurer! + """ + Monthly price of the external insurance. Null if not available. + """ + price: Money + """ + Contextual subtitle (e.g. address, registration number, pet name) + """ + subtitle: String + """ + Renewal date of the external policy. Null when not provided. + """ + renewalDate: Date + """ + Insurely data collection ID + """ + dataCollectionId: String! +} type RecommendedCrossSell { crossSell: CrossSell! bannerText: String! diff --git a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml index a61bb30445..8131042d0e 100644 --- a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml @@ -80,7 +80,6 @@ Skanna QR-koden med BankID-appen på din telefon, eller logga in med din e-postadress nedan. Utbetalning till ett svenskt bankkonto Bankkonto - Clearingnummer Hej - just nu har vi lunch-stängt i chatten. Vi svarar så fort vi kan när vi öppnar igen klockan 13. Tack för ditt meddelande. Vi svarar så snart som möjligt. Just nu är chatten stängd. Vi svarar så fort vi kan när vi öppnar. @@ -607,6 +606,8 @@ Om du uppdaterar ditt bankkonto nära den %1$s kan det ta några extra dagar innan pengarna dras. Denna betalning misslyckades och lades till i din betalning den %1$s . Betalningshistorik + Betalning pågår + Det kan ta upp till 5 bankdagar innan betalningen syns Betalningssätt Att betala: %1$s Vi kunde inte dra betalningen från ditt bankkonto. Betala för att undvika avbrott i ditt skydd. @@ -616,7 +617,7 @@ Se till att det finns tillräckliga medel på kontot för att genomföra betalningen Betala %1$s Försenad sedan %1$s - Visa betalningsinformation + Visa betalningsdetaljer Försenad betalning Betalning genomförd %1$s dagar diff --git a/app/core/core-resources/src/androidMain/res/values/strings.xml b/app/core/core-resources/src/androidMain/res/values/strings.xml index 22400fff12..62369164f1 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -80,7 +80,6 @@ Scan the QR-code with the BankID app on the phone where it’s installed or log in with email using the button below. Payout to a Swedish bank account Bank account - Clearing number Hej - just nu har vi lunch-stängt i chatten. Vi svarar så fort vi kan när vi öppnar igen klockan 13. Thank you for reaching out to us. We will answer as soon as possible. Hej - just nu är chatten stängd. Vi svarar så fort vi kan när vi öppnar. @@ -607,6 +606,8 @@ If you update your bank account close to the %1$s, the withdrawal might take a few extra days. This payment failed and was added to your payment on %1$s. Payment history + Payment in progress + It may take up to 5 business days for the charge to appear Payment method Amount due: %1$s We couldn\'t collect this payment from your bank account. Pay now to keep your coverage active. diff --git a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml index dfaf17c780..d031beede6 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -80,7 +80,6 @@ Skanna QR-koden med BankID-appen på din telefon, eller logga in med din e-postadress nedan. Utbetalning till ett svenskt bankkonto Bankkonto - Clearingnummer Hej - just nu har vi lunch-stängt i chatten. Vi svarar så fort vi kan när vi öppnar igen klockan 13. Tack för ditt meddelande. Vi svarar så snart som möjligt. Just nu är chatten stängd. Vi svarar så fort vi kan när vi öppnar. @@ -607,6 +606,8 @@ Om du uppdaterar ditt bankkonto nära den %1$s kan det ta några extra dagar innan pengarna dras. Denna betalning misslyckades och lades till i din betalning den %1$s . Betalningshistorik + Betalning pågår + Det kan ta upp till 5 bankdagar innan betalningen syns Betalningssätt Att betala: %1$s Vi kunde inte dra betalningen från ditt bankkonto. Betala för att undvika avbrott i ditt skydd. @@ -616,7 +617,7 @@ Se till att det finns tillräckliga medel på kontot för att genomföra betalningen Betala %1$s Försenad sedan %1$s - Visa betalningsinformation + Visa betalningsdetaljer Försenad betalning Betalning genomförd %1$s dagar diff --git a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml index 73bff99a7c..12f8ea7319 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -80,7 +80,6 @@ Scan the QR-code with the BankID app on the phone where it’s installed or log in with email using the button below. Payout to a Swedish bank account Bank account - Clearing number Hej - just nu har vi lunch-stängt i chatten. Vi svarar så fort vi kan när vi öppnar igen klockan 13. Thank you for reaching out to us. We will answer as soon as possible. Hej - just nu är chatten stängd. Vi svarar så fort vi kan när vi öppnar. @@ -607,6 +606,8 @@ If you update your bank account close to the %1$s, the withdrawal might take a few extra days. This payment failed and was added to your payment on %1$s. Payment history + Payment in progress + It may take up to 5 business days for the charge to appear Payment method Amount due: %1$s We couldn't collect this payment from your bank account. Pay now to keep your coverage active. diff --git a/app/feature/feature-payments/src/main/graphql/MutationManuallyChargeMember.graphql b/app/feature/feature-payments/src/main/graphql/MutationManuallyChargeMember.graphql new file mode 100644 index 0000000000..f50621c8bb --- /dev/null +++ b/app/feature/feature-payments/src/main/graphql/MutationManuallyChargeMember.graphql @@ -0,0 +1,7 @@ +mutation ManuallyChargeMember($dueDate: Date!) { + manuallyChargeMember(dueDate: $dueDate) { + userError { + message + } + } +} diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt index 68aa5b5bd9..c3c26b4da3 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt @@ -1,11 +1,32 @@ package com.hedvig.android.feature.payments.data +import arrow.core.Either +import arrow.core.raise.context.bind +import arrow.core.raise.context.either +import arrow.core.raise.context.raise +import com.apollographql.apollo.ApolloClient +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.apollo.ErrorMessage +import kotlinx.datetime.LocalDate +import octopus.ManuallyChargeMemberMutation + internal interface TriggerManualChargeUseCase { - suspend fun invoke() + suspend fun invoke(dueDate: LocalDate): Either } -internal class TriggerManualChargeUseCaseImpl: TriggerManualChargeUseCase { - override suspend fun invoke() { - TODO("Not yet implemented") +internal class TriggerManualChargeUseCaseImpl( + private val apolloClient: ApolloClient +): TriggerManualChargeUseCase { + override suspend fun invoke(dueDate: LocalDate): Either = either { + val result = apolloClient + .mutation(ManuallyChargeMemberMutation(dueDate)) + .safeExecute() + .mapLeft(::ErrorMessage) + .bind() + + if (result.manuallyChargeMember.userError!=null) raise(ErrorMessage( + result.manuallyChargeMember.userError.message + )) else Unit } } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt index bcbddfeea4..f2b93e6f3a 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt @@ -126,7 +126,7 @@ val paymentsModule = module { } single { - TriggerManualChargeUseCaseImpl() + TriggerManualChargeUseCaseImpl(get(),) } single { diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt index 11f7bccad2..5dbdbf7dbb 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt @@ -31,5 +31,8 @@ internal sealed interface PaymentsDestinations { @Serializable data object ManualCharge: PaymentsDestinations, Destination + + @Serializable + data object ManualChargeSuccess: PaymentsDestinations, Destination } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt index e7fc379892..469042d92d 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt @@ -13,6 +13,7 @@ import com.hedvig.android.feature.payments.ui.discounts.DiscountsViewModel import com.hedvig.android.feature.payments.ui.history.PaymentHistoryDestination import com.hedvig.android.feature.payments.ui.history.PaymentHistoryViewModel import com.hedvig.android.feature.payments.ui.manualcharge.ManualChargeDestination +import com.hedvig.android.feature.payments.ui.manualcharge.ManualChargeSuccessDestination import com.hedvig.android.feature.payments.ui.manualcharge.ManualChargeViewModel import com.hedvig.android.feature.payments.ui.memberpaymentdetails.MemberPaymentDetailsDestination import com.hedvig.android.feature.payments.ui.memberpaymentdetails.MemberPaymentDetailsViewModel @@ -22,6 +23,7 @@ import com.hedvig.android.language.LanguageService import com.hedvig.android.navigation.compose.navDeepLinks import com.hedvig.android.navigation.compose.navdestination import com.hedvig.android.navigation.compose.navgraph +import com.hedvig.android.navigation.compose.typedPopUpTo import com.hedvig.android.navigation.core.HedvigDeepLinkContainer import com.hedvig.android.shared.foreverui.ui.ui.ForeverDestination import com.hedvig.android.shared.foreverui.ui.ui.ForeverViewModel @@ -60,7 +62,7 @@ fun NavGraphBuilder.paymentsGraph( navController.navigate(PaymentsDestinations.MemberPaymentDetails) }, onOpenManualCharge = { - // todo + navController.navigate(PaymentsDestinations.ManualCharge) }, ) } @@ -78,10 +80,21 @@ fun NavGraphBuilder.paymentsGraph( chargeId, ), ) + }, + onNavigateToSuccess = { + navController.navigate(PaymentsDestinations.ManualChargeSuccess) { + typedPopUpTo { + inclusive = true + } + } } ) } + navdestination{ + ManualChargeSuccessDestination(navController::navigateUp) + } + navdestination { val viewModel: PaymentDetailsViewModel = koinViewModel(parameters = { parametersOf(this.memberChargeId) }) PaymentDetailsDestination( 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 fe46b93b5d..8c888d24ad 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 @@ -22,6 +22,7 @@ import com.hedvig.android.feature.payments.data.PaymentOverview.OngoingCharge import com.hedvig.android.feature.payments.data.toChargeMethod import com.hedvig.android.feature.payments.data.toFailedCharge import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.logger.logcat import kotlin.time.Clock import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.seconds @@ -55,9 +56,11 @@ internal data class GetUpcomingPaymentUseCaseImpl( emitAll( apolloClient.query(UpcomingPaymentQuery()) .fetchPolicy(FetchPolicy.NetworkFirst) - .safeFlow(::ErrorMessage) + .safeFlow { + logcat { "GetUpcomingPaymentUseCaseImpl error: $it" } + ErrorMessage() + } .map { response -> - either { val result = response.bind() val paymentConnection = run { diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt index 22e31bd0b6..6c0041e3f4 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt @@ -12,6 +12,7 @@ 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.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -19,6 +20,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney import com.hedvig.android.design.system.hedvig.ButtonDefaults @@ -38,6 +40,8 @@ import com.hedvig.android.design.system.hedvig.icon.WarningFilled import com.hedvig.android.design.system.hedvig.rememberHedvigDateTimeFormatter import com.hedvig.android.design.system.hedvig.rememberHedvigMonthDateTimeFormatter import com.hedvig.android.feature.payments.data.ManualChargeInfo +import hedvig.resources.GENERAL_ERROR_BODY +import hedvig.resources.GENERAL_RETRY import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_BODY import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_DUE_DATE import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_FINE_PRINT @@ -46,6 +50,7 @@ import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_SINCE import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_VIEW_DETAILS import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_TITLE import hedvig.resources.Res +import hedvig.resources.general_close_button import hedvig.resources.payment_details_receipt_card_total import kotlinx.datetime.LocalDate import org.jetbrains.compose.resources.stringResource @@ -55,6 +60,7 @@ internal fun ManualChargeDestination( viewModel: ManualChargeViewModel, navigateUp: () -> Unit, onNavigateToPaymentDetails: (chargeId: String) -> Unit, + onNavigateToSuccess: () -> Unit, ) { val uiState = viewModel.uiState.collectAsStateWithLifecycle() @@ -62,7 +68,13 @@ internal fun ManualChargeDestination( uiState = uiState.value, navigateUp = navigateUp, reload = { viewModel.emit(ManualChargeEvent.Retry) }, - onNavigateToPaymentDetails = onNavigateToPaymentDetails + onNavigateToPaymentDetails = onNavigateToPaymentDetails, + onNavigateToSuccess = { + viewModel.emit(ManualChargeEvent.ClearNav) + onNavigateToSuccess() + }, + onTriggerPayment = { + viewModel.emit(ManualChargeEvent.TriggerCharge) } ) } @@ -72,6 +84,8 @@ private fun ManualChargeScreen( navigateUp: () -> Unit, reload: () -> Unit, onNavigateToPaymentDetails: (chargeId: String) -> Unit, + onNavigateToSuccess: () -> Unit, + onTriggerPayment: () -> Unit ) { HedvigScaffold( navigateUp = navigateUp, @@ -80,10 +94,17 @@ private fun ManualChargeScreen( when (uiState) { is ManualChargeUiState.Failure -> { - //todo + val subTitle = if (uiState.error.message!=null) uiState.error.message else + stringResource(Res.string.GENERAL_ERROR_BODY) + val buttonText = if (uiState.error.message!=null) stringResource(Res.string.general_close_button) else + stringResource(Res.string.GENERAL_RETRY) + val onButtonClick = if (uiState.error.message!=null) navigateUp else reload + HedvigErrorSection( - onButtonClick = reload, - Modifier.weight(1f), + onButtonClick = onButtonClick, + Modifier.weight(1f).fillMaxWidth(), + subTitle = subTitle, + buttonText = buttonText ) } @@ -95,10 +116,17 @@ private fun ManualChargeScreen( } is ManualChargeUiState.Success -> { - ManualChargeSuccessScreen( - uiState, - onNavigateToPaymentDetails = onNavigateToPaymentDetails - ) + if (uiState.navigateToSuccess!=null) { + LaunchedEffect(uiState.navigateToSuccess) { + onNavigateToSuccess() + } + } else { + ManualChargeSuccessScreen( + uiState, + onNavigateToPaymentDetails = onNavigateToPaymentDetails, + onTriggerPayment = onTriggerPayment + ) + } } } } @@ -108,6 +136,7 @@ private fun ManualChargeScreen( private fun ManualChargeSuccessScreen( uiState: ManualChargeUiState.Success, onNavigateToPaymentDetails: (chargeId: String) -> Unit, + onTriggerPayment: () -> Unit, ) { val dateTimeFormatter = rememberHedvigMonthDateTimeFormatter() val dateTimeFormatterWithYear = rememberHedvigDateTimeFormatter() @@ -236,7 +265,7 @@ private fun ManualChargeSuccessScreen( HedvigButton( text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_PAY, uiState.manualChargeInfo.amountDue), - onClick = { /* TODO: Handle payment */ }, + onClick = onTriggerPayment, enabled = true, modifier = Modifier.fillMaxWidth(), ) @@ -269,11 +298,14 @@ private fun ManualChargeScreenSuccessPreview() { chargeId = "chargeId", bankDescriptor = "Bank account", bankAccountDisplayValue = "**** 8324" - ) + ), + navigateToSuccess = null ), navigateUp = {}, reload = {}, - {} + {}, + {}, + {}, ) } } @@ -289,7 +321,9 @@ private fun ManualChargeScreenLoadingPreview() { uiState = ManualChargeUiState.Loading, navigateUp = {}, reload = {}, - {} + {}, + {}, + {}, ) } } @@ -302,10 +336,12 @@ private fun ManualChargeScreenFailurePreview() { HedvigTheme { Surface { ManualChargeScreen( - uiState = ManualChargeUiState.Failure(ManualChargeFailureReason.GeneralFailure), + uiState = ManualChargeUiState.Failure(ErrorMessage("Payment method not allowed")), navigateUp = {}, reload = {}, - {} + {}, + {}, + {}, ) } } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt new file mode 100644 index 0000000000..05ad0426c0 --- /dev/null +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt @@ -0,0 +1,69 @@ +package com.hedvig.android.feature.payments.ui.manualcharge + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.dropUnlessResumed +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize.Large +import com.hedvig.android.design.system.hedvig.EmptyState +import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateButtonStyle.NoButton +import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateIconStyle.SUCCESS +import com.hedvig.android.design.system.hedvig.HedvigPreview +import com.hedvig.android.design.system.hedvig.HedvigTextButton +import hedvig.resources.PAYMENTS_PAYMENT_IN_PROGRESS +import hedvig.resources.PAYMENTS_PAYMENT_IN_PROGRESS_DESCRIPTION +import hedvig.resources.Res +import hedvig.resources.general_close_button +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun ManualChargeSuccessDestination(popBackStack: () -> Unit) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + .windowInsetsPadding( + WindowInsets.safeDrawing.only( + WindowInsetsSides.Horizontal + + WindowInsetsSides.Bottom, + ), + ), + ) { + Spacer(Modifier.weight(1f)) + EmptyState( + modifier = Modifier.fillMaxWidth(), + text = stringResource(Res.string.PAYMENTS_PAYMENT_IN_PROGRESS), + description = stringResource( + Res.string.PAYMENTS_PAYMENT_IN_PROGRESS_DESCRIPTION, + ), + iconStyle = SUCCESS, + buttonStyle = NoButton, + ) + Spacer(Modifier.weight(1f)) + HedvigTextButton( + stringResource(Res.string.general_close_button), + onClick = dropUnlessResumed { popBackStack() }, + buttonSize = Large, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(16.dp)) + } +} + +@HedvigPreview +@Composable +private fun ManualChargeSuccessDestinationPreview() { + ManualChargeSuccessDestination({}) +} diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt index 263a124e26..20028de14a 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt @@ -1,6 +1,13 @@ package com.hedvig.android.feature.payments.ui.manualcharge import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.uidata.UiMoney import com.hedvig.android.feature.payments.data.GetManualChargeInfoUseCase import com.hedvig.android.feature.payments.data.ManualChargeInfo @@ -26,8 +33,47 @@ private class ManualChargePresenter( override fun MoleculePresenterScope.present( lastState: ManualChargeUiState, ): ManualChargeUiState { - // TODO: Implement presenter logic - return ManualChargeUiState.Loading + var dataLoadIteration by remember { mutableIntStateOf(0) } + var screenState by remember { mutableStateOf(lastState) } + var triggerChargeIteration by remember { mutableIntStateOf(0) } + + CollectEvents { + when (it) { + ManualChargeEvent.Retry -> dataLoadIteration++ + ManualChargeEvent.TriggerCharge -> triggerChargeIteration++ + ManualChargeEvent.ClearNav -> { + val currentState = screenState as? ManualChargeUiState.Success ?: return@CollectEvents + screenState = currentState.copy(navigateToSuccess = null) + } + } + } + + LaunchedEffect(triggerChargeIteration) { + if (triggerChargeIteration>0) { + val currentState = screenState as? ManualChargeUiState.Success ?: return@LaunchedEffect + triggerManualCharge.invoke(currentState.manualChargeInfo.missedDueDate).fold( + ifLeft = { + screenState = ManualChargeUiState.Failure(it) + }, + ifRight = { + screenState = ManualChargeUiState.Success(currentState.manualChargeInfo, Unit) + } + ) + } + } + + LaunchedEffect(dataLoadIteration) { + screenState = ManualChargeUiState.Loading + getManualChargeInfoUseCase.invoke().fold( + ifRight = { manualChargeInfo -> + screenState = ManualChargeUiState.Success(manualChargeInfo, null) + }, + ifLeft = { failure -> + screenState = ManualChargeUiState.Failure(failure) + }, + ) + } + return screenState } } @@ -35,25 +81,19 @@ internal sealed interface ManualChargeUiState { data object Loading : ManualChargeUiState data class Failure( - val reason: ManualChargeFailureReason + val error: ErrorMessage ) : ManualChargeUiState data class Success( - val manualChargeInfo: ManualChargeInfo + val manualChargeInfo: ManualChargeInfo, + val navigateToSuccess: Unit? ) : ManualChargeUiState } -internal interface ManualChargeFailureReason { - data object NotAllowed: ManualChargeFailureReason - data object GeneralFailure: ManualChargeFailureReason - data class UserErrorWithMessage( - val message: String - ): ManualChargeFailureReason -} - internal sealed interface ManualChargeEvent { data object Retry : ManualChargeEvent - // TODO: Add events + data object TriggerCharge : ManualChargeEvent + data object ClearNav : ManualChargeEvent } diff --git a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/di/NotificationBadgeModule.kt b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/di/NotificationBadgeModule.kt index e9528a0a62..3ff74b26cf 100644 --- a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/di/NotificationBadgeModule.kt +++ b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/di/NotificationBadgeModule.kt @@ -11,8 +11,8 @@ import com.hedvig.android.notification.badge.data.crosssell.home.CrossSellHomeNo import com.hedvig.android.notification.badge.data.crosssell.home.CrossSellHomeNotificationServiceProvider import com.hedvig.android.notification.badge.data.crosssell.home.DemoCrossSellHomeNotificationService import com.hedvig.android.notification.badge.data.payment.DemoMissedPaymentNotificationService -import com.hedvig.android.notification.badge.data.payment.GetMissedPaymentIdUseCase -import com.hedvig.android.notification.badge.data.payment.GetMissedPaymentIdUseCaseImpl +import com.hedvig.android.notification.badge.data.payment.GetIfMissedPaymentUseCase +import com.hedvig.android.notification.badge.data.payment.GetIfMissedPaymentUseCaseImpl import com.hedvig.android.notification.badge.data.payment.MissedPaymentNotificationServiceImpl import com.hedvig.android.notification.badge.data.payment.MissedPaymentNotificationServiceProvider import com.hedvig.android.notification.badge.data.storage.DatastoreNotificationBadgeStorage @@ -46,10 +46,10 @@ val notificationBadgeModule = module { prodImpl = get(), ) } - single { - GetMissedPaymentIdUseCaseImpl(get()) + single { + GetIfMissedPaymentUseCaseImpl(get()) } single { - MissedPaymentNotificationServiceImpl(get()) + MissedPaymentNotificationServiceImpl(get()) } } diff --git a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetIfMissedPaymentUseCase.kt b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetIfMissedPaymentUseCase.kt new file mode 100644 index 0000000000..bf69eb6738 --- /dev/null +++ b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetIfMissedPaymentUseCase.kt @@ -0,0 +1,53 @@ +package com.hedvig.android.notification.badge.data.payment + +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.cache.normalized.FetchPolicy +import com.apollographql.apollo.cache.normalized.fetchPolicy +import com.hedvig.android.apollo.safeFlow +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.logger.logcat +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.isActive +import octopus.MissedPaymentQuery + +interface GetIfMissedPaymentUseCase { + fun invoke(): Flow +} + +internal class GetIfMissedPaymentUseCaseImpl( + private val apolloClient: ApolloClient, +) : GetIfMissedPaymentUseCase { + override fun invoke(): Flow { + return flow { + while (currentCoroutineContext().isActive) { + emitAll( + apolloClient + .query(MissedPaymentQuery()) + .fetchPolicy(FetchPolicy.CacheAndNetwork) + .safeFlow { + logcat { "GetIfMissedPaymentUseCaseImpl error: $it" } + ErrorMessage() + } + .map { result -> + result.fold( + { + logcat { "GetIfMissedPaymentUseCaseImpl: error when loading missed payment: $it" } + false + }, + { data -> + data.currentMember.missedChargeIdToChargeManually != null + }, + ) + }, + ) + delay(5.seconds) + } + } + } +} diff --git a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetMissedPaymentIdUseCase.kt b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetMissedPaymentIdUseCase.kt deleted file mode 100644 index 2ddc921178..0000000000 --- a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetMissedPaymentIdUseCase.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.hedvig.android.notification.badge.data.payment - -import com.apollographql.apollo.ApolloClient -import com.apollographql.apollo.cache.normalized.FetchPolicy -import com.apollographql.apollo.cache.normalized.fetchPolicy -import com.hedvig.android.apollo.safeFlow -import com.hedvig.android.logger.logcat -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import octopus.MissedPaymentQuery - -interface GetMissedPaymentIdUseCase { - fun invoke(): Flow -} - -internal class GetMissedPaymentIdUseCaseImpl( - private val apolloClient: ApolloClient, -) : GetMissedPaymentIdUseCase { - override fun invoke(): Flow { - return apolloClient - .query(MissedPaymentQuery()) - .fetchPolicy(FetchPolicy.NetworkOnly) - .safeFlow() - .map { result -> - result.fold( - { - logcat(operationError = it) { - "Error when loading missed payment: $it" - } - false - }, - { data -> - data.currentMember.missedChargeIdToChargeManually != null - }, - ) - } - } -} diff --git a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/MissedPaymentNotificationService.kt b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/MissedPaymentNotificationService.kt index 8d33c7473c..0000e0ffb0 100644 --- a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/MissedPaymentNotificationService.kt +++ b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/MissedPaymentNotificationService.kt @@ -24,9 +24,9 @@ internal class DemoMissedPaymentNotificationService : MissedPaymentNotificationS } internal class MissedPaymentNotificationServiceImpl( - private val getMissedPaymentIdUseCase: GetMissedPaymentIdUseCase, + private val getIfMissedPaymentUseCase: GetIfMissedPaymentUseCase, ) : MissedPaymentNotificationService { override fun showRedDotNotification(): Flow { - return getMissedPaymentIdUseCase.invoke() + return getIfMissedPaymentUseCase.invoke() } } From b2142eff869ac9cce9ef36c40953752ee3c9b8cc Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 29 Apr 2026 17:12:04 +0200 Subject: [PATCH 14/29] change api --- .../com/hedvig/android/apollo/octopus/schema.graphqls | 2 +- .../src/main/graphql/MutationManuallyChargeMember.graphql | 4 ++-- .../feature/payments/data/TriggerManualChargeUseCase.kt | 8 ++++---- .../payments/ui/manualcharge/ManualChargeViewModel.kt | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) 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 a925c8226d..45b72a15fa 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 @@ -3483,7 +3483,7 @@ input MoveToHouseInput { } type Mutation { registerDirectDebit2(clientContext: RegisterDirectDebitClientContext2): DirectDebitResponse2! - manuallyChargeMember(dueDate: Date!): ManuallyChargeMemberMutationOutput! + manuallyChargeMember: ManuallyChargeMemberMutationOutput! """ Setup invoice payment method for the member. Kivra will be used as the provider if supported, else mail. """ diff --git a/app/feature/feature-payments/src/main/graphql/MutationManuallyChargeMember.graphql b/app/feature/feature-payments/src/main/graphql/MutationManuallyChargeMember.graphql index f50621c8bb..8767d2322a 100644 --- a/app/feature/feature-payments/src/main/graphql/MutationManuallyChargeMember.graphql +++ b/app/feature/feature-payments/src/main/graphql/MutationManuallyChargeMember.graphql @@ -1,5 +1,5 @@ -mutation ManuallyChargeMember($dueDate: Date!) { - manuallyChargeMember(dueDate: $dueDate) { +mutation ManuallyChargeMember { + manuallyChargeMember { userError { message } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt index c3c26b4da3..0b4e4194ca 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt @@ -12,17 +12,17 @@ import kotlinx.datetime.LocalDate import octopus.ManuallyChargeMemberMutation internal interface TriggerManualChargeUseCase { - suspend fun invoke(dueDate: LocalDate): Either + suspend fun invoke(): Either } internal class TriggerManualChargeUseCaseImpl( private val apolloClient: ApolloClient ): TriggerManualChargeUseCase { - override suspend fun invoke(dueDate: LocalDate): Either = either { + override suspend fun invoke(): Either = either { val result = apolloClient - .mutation(ManuallyChargeMemberMutation(dueDate)) + .mutation(ManuallyChargeMemberMutation()) .safeExecute() - .mapLeft(::ErrorMessage) + .mapLeft { raise(ErrorMessage()) } .bind() if (result.manuallyChargeMember.userError!=null) raise(ErrorMessage( diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt index 20028de14a..1d2244377b 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt @@ -51,7 +51,7 @@ private class ManualChargePresenter( LaunchedEffect(triggerChargeIteration) { if (triggerChargeIteration>0) { val currentState = screenState as? ManualChargeUiState.Success ?: return@LaunchedEffect - triggerManualCharge.invoke(currentState.manualChargeInfo.missedDueDate).fold( + triggerManualCharge.invoke().fold( ifLeft = { screenState = ManualChargeUiState.Failure(it) }, From 242d4c8c7a6806c27c6fbc833ac7ef90e5d69891 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 6 May 2026 11:09:54 +0200 Subject: [PATCH 15/29] finish merging --- .../android/apollo/octopus/schema.graphqls | 98 ++++++++- .../androidMain/res/values-sv-rSE/strings.xml | 2 + .../src/androidMain/res/values/strings.xml | 2 + .../values-sv-rSE/strings.xml | 2 + .../composeResources/values/strings.xml | 2 + .../data/GetManualChargeInfoUseCase.kt | 23 +- .../feature/payments/di/PaymentsModule.kt | 3 - .../data/GetUpcomingPaymentUseCase.kt | 201 ++++++------------ .../ui/payments/PaymentsDestination.kt | 2 + .../payments/ui/payments/PaymentsPresenter.kt | 3 +- 10 files changed, 178 insertions(+), 160 deletions(-) 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 96972a992d..a3e09a71dd 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 @@ -2527,6 +2527,11 @@ type Member { """ crossSellV2(input: CrossSellInput!): CrossSellV2! """ + Young Pet Guide stories for the member. + Returns a list of educational content stories for young pet owners. + """ + puppyGuideStories: [PuppyGuideStory!]! + """ Fetch all the active contracts for this member. Active contracts include all insurances that are either active today, or to-be-active in the future. """ @@ -3501,7 +3506,7 @@ type Mutation { """ paymentMethodSetupSwishPayout(input: PaymentMethodSetupSwishInput!): PaymentMethodSetupOutput! """ - Setup Swish payin method for the member. Requires member consent in Swish app. + Setup Swish payin method for the member. Requires member consent in Swish app. """ paymentMethodSetupSwishPayin(input: PaymentMethodSetupSwishInput!): PaymentMethodSetupOutput! """ @@ -3650,8 +3655,10 @@ type Mutation { """ Confirm this PriceIntent, which will use the current data (and likely `ShopSession.customer`) to generate `ProductOffers` that can be added to the cart. + 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!): PriceIntentMutationOutput! + priceIntentConfirm(priceIntentId: UUID!, attribution: PriceIntentConfirmAttributionInput): 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. @@ -3673,6 +3680,10 @@ type Mutation { """ productOfferReprice(offerId: UUID!, data: PricingFormData!): ProductOffersMutationOutput! """ + Mark a young pet guide story as read for a specific member. + """ + puppyGuideEngagement(engagement: PuppyEngagementInput!): PuppyGuideStoryMutationOutput! + """ Update the customer of the shop session. Only non-null fields will be changed. Can trigger automatic lookup of other information. The session can be placed in a "point of no return" state where it is no longer legal to update the customer, @@ -3797,7 +3808,7 @@ type PartnerWidgetTrial { } type PaymentMethodBankAccountDetails { """ - The bank account reference - e.g. clearing number + account number. + The bank account reference - e.g. clearing number + account number. """ account: String! """ @@ -3832,11 +3843,11 @@ type PaymentMethodSetupOutput { """ status: PaymentMethodSetupStatus! """ - The order id for the payment method setup order if SUCCESSFUL. + The order id for the payment method setup order if SUCCESSFUL. """ orderId: ID """ - Url to redirect the member to if any. + Url to redirect the member to if any. """ url: String """ @@ -4015,6 +4026,36 @@ type PriceIntentAnimalBreed { displayName: String! isMixedBreed: Boolean! } +""" +Attribution values to apply when confirming a PriceIntent. +Provided fields override the ShopSession attribution for this confirmation only. +""" +input PriceIntentConfirmAttributionInput { + """ + Party credited for originating the flow, such as a partner, Hedvig, or marketing source. + """ + attributedTo: String + """ + Channel or surface where the flow was initiated. + """ + initiatedFrom: String + """ + High-level business flow used for attribution and analytics. + """ + userFlow: String + """ + More specific source within the user flow, when applicable. + """ + flowSource: String + """ + Experiment variants active when the price intent was confirmed. + """ + experiments: [ShopSessionExperimentInput!] + """ + Additional attribution metadata passed through for analytics integrations. + """ + trackingData: JSON +} input PriceIntentCreateInput { shopSessionId: UUID! """ @@ -4404,6 +4445,53 @@ type ProductVariantComparisonRow { """ covered: [String!]! } +input PuppyEngagementInput { + name: String! + rating: Int + opened: Boolean + read: Boolean + closed: Boolean +} +type PuppyGuideStory { + """ + The unique name/identifier of the story. + """ + name: String! + """ + The display title of the story. + """ + title: String! + """ + The subtitle or description of the story. + """ + subtitle: String! + """ + The main content of the story. + """ + content: String! + """ + The image associated with this story. + """ + image: String! + """ + Categories this story belongs to. + """ + categories: [String!]! + """ + The date when the story was marked as read by the user. + """ + read: Boolean! + """ + The user's rating of the story. + """ + rating: Int +} +type PuppyGuideStoryMutationOutput { + """ + Indicates whether the mutation was successful. + """ + success: Boolean! +} type Query { """ Return a conversation for a given ID. diff --git a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml index d0f3517425..8adea1e591 100644 --- a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml @@ -620,6 +620,8 @@ Betala %1$s Försenad sedan %1$s Visa betalningsdetaljer + Inga försenade betalningar + Allt är betalt Försenad betalning Betalning genomförd %1$s dagar diff --git a/app/core/core-resources/src/androidMain/res/values/strings.xml b/app/core/core-resources/src/androidMain/res/values/strings.xml index 853c6f6747..6d855e8fcd 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -620,6 +620,8 @@ Pay %1$s Overdue since %1$s View payment details + No overdue payments + Payments up to date Payment overdue Payment successful %1$s days diff --git a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml index b2bd53ce12..d5254ac25d 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -620,6 +620,8 @@ Betala %1$s Försenad sedan %1$s Visa betalningsdetaljer + Inga försenade betalningar + Allt är betalt Försenad betalning Betalning genomförd %1$s dagar diff --git a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml index cb2684c667..f36a16313e 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -620,6 +620,8 @@ Pay %1$s Overdue since %1$s View payment details + No overdue payments + Payments up to date Payment overdue Payment successful %1$s days diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt index 9d0151971c..5e1d097080 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt @@ -11,14 +11,9 @@ import com.hedvig.android.apollo.ErrorMessage import com.hedvig.android.apollo.safeExecute import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.uidata.UiMoney -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.featureflags.flags.Feature -import kotlinx.coroutines.flow.first +import com.hedvig.android.logger.logcat import kotlinx.datetime.LocalDate import octopus.ManualChargeInfoQuery -import octopus.type.MemberChargeStatus -import com.hedvig.android.logger.logcat -import octopus.type.MemberPaymentConnectionStatus internal interface GetManualChargeInfoUseCase { suspend fun invoke(): Either @@ -26,7 +21,7 @@ internal interface GetManualChargeInfoUseCase { internal class GetManualChargeInfoUseCaseImpl( private val apolloClient: ApolloClient, -): GetManualChargeInfoUseCase { +) : GetManualChargeInfoUseCase { override suspend fun invoke(): Either = either { val currentMember = apolloClient.query(ManualChargeInfoQuery()) @@ -37,16 +32,16 @@ internal class GetManualChargeInfoUseCaseImpl( val showManualCharge = currentMember.missedChargeIdToChargeManually - if (showManualCharge==null) { - logcat {"GetManualChargeInfoUseCaseImpl: missedChargeIdToChargeManually is null"} + if (showManualCharge == null) { + logcat { "GetManualChargeInfoUseCaseImpl: missedChargeIdToChargeManually is null" } raise(ErrorMessage()) } val latestFailedPastCharge = currentMember.pastCharges - .firstOrNull {it.id == showManualCharge} + .firstOrNull { it.id == showManualCharge } - if (latestFailedPastCharge==null) { - logcat {"GetManualChargeInfoUseCaseImpl: latestFailedPastCharge is null"} + if (latestFailedPastCharge == null) { + logcat { "GetManualChargeInfoUseCaseImpl: latestFailedPastCharge is null" } raise(ErrorMessage()) } @@ -55,7 +50,7 @@ internal class GetManualChargeInfoUseCaseImpl( missedDueDate = latestFailedPastCharge.date, amountDue = UiMoney.fromMoneyFragment(latestFailedPastCharge.net), bankAccountDisplayValue = currentMember.paymentInformation.chargeMethod?.displayName, - bankDescriptor = currentMember.paymentInformation.chargeMethod?.descriptor + bankDescriptor = currentMember.paymentInformation.chargeMethod?.descriptor, ) } } @@ -65,5 +60,5 @@ internal data class ManualChargeInfo( val missedDueDate: LocalDate, val amountDue: UiMoney, val bankDescriptor: String?, - val bankAccountDisplayValue: String? + val bankAccountDisplayValue: String?, ) diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt index bd0c06239c..c50b419ca6 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt @@ -33,7 +33,6 @@ import com.hedvig.android.feature.payments.ui.history.PaymentHistoryViewModel import com.hedvig.android.feature.payments.ui.manualcharge.ManualChargeViewModel import com.hedvig.android.feature.payments.ui.memberpaymentdetails.MemberPaymentDetailsViewModel import com.hedvig.android.feature.payments.ui.payments.PaymentsViewModel -import com.hedvig.android.featureflags.FeatureManager import kotlin.time.Clock import org.koin.core.module.dsl.viewModel import org.koin.dsl.module @@ -58,7 +57,6 @@ val paymentsModule = module { GetUpcomingPaymentUseCaseImpl( apolloClient = get(), clock = get(), - featureManager = get(), ) } single { @@ -121,7 +119,6 @@ val paymentsModule = module { GetUpcomingPaymentUseCaseImpl( get(), clock = get(), - featureManager = get(), ) } single { 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 5598c50217..7e660417d7 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 @@ -7,33 +7,19 @@ import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.cache.normalized.FetchPolicy import com.apollographql.apollo.cache.normalized.fetchPolicy import com.hedvig.android.apollo.ErrorMessage -import com.hedvig.android.apollo.safeFlow +import com.hedvig.android.apollo.safeExecute import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney import com.hedvig.android.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.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.PaymentOverview import com.hedvig.android.feature.payments.data.PaymentOverview.OngoingCharge -import com.hedvig.android.feature.payments.data.toChargeMethod import com.hedvig.android.feature.payments.data.toFailedCharge -import com.hedvig.android.featureflags.FeatureManager -import com.hedvig.android.logger.logcat import kotlin.time.Clock import kotlin.time.Duration.Companion.days -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.isActive import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import octopus.UpcomingPaymentQuery @@ -42,121 +28,64 @@ import octopus.type.MemberChargeStatus import octopus.type.MemberPaymentMethodStatus internal interface GetUpcomingPaymentUseCase { - suspend fun invoke(): Flow> + suspend fun invoke(): Either } internal data class GetUpcomingPaymentUseCaseImpl( val apolloClient: ApolloClient, - val featureManager: FeatureManager, val clock: Clock, ) : GetUpcomingPaymentUseCase { - override suspend fun invoke(): Flow> { - return flow { - while (currentCoroutineContext().isActive) { - emitAll( - apolloClient.query(UpcomingPaymentQuery()) - .fetchPolicy(FetchPolicy.NetworkFirst) - .safeFlow { - logcat { "GetUpcomingPaymentUseCaseImpl error: $it" } - ErrorMessage() - } - .map { response -> - either { - val result = response.bind() - - - - PaymentOverview( - memberChargeShortInfo = result.currentMember.futureCharge?.toMemberChargeShortInfo(), - ongoingCharges = result.currentMember.ongoingCharges.mapNotNull { - val id = it.id ?: return@mapNotNull null - OngoingCharge(id, it.date, UiMoney.fromMoneyFragment(it.net)) - }, - paymentConnection = run { - val paymentMethods = result.currentMember.paymentMethods - val payinMethod = paymentMethods.defaultPayinMethod - ?: paymentMethods.payinMethods.find { it.isDefault } - if (payinMethod == null) { - val firstKnownTerminationDateForContractTerminatedDueToMissedPayments = result - .currentMember - .activeContracts - .filter { it.terminationDueToMissedPayments } - .mapNotNull { it.terminationDate } - .sorted() - .firstOrNull() - return@run PaymentConnection.NeedsSetup(firstKnownTerminationDateForContractTerminatedDueToMissedPayments) - } - when (payinMethod.status) { - MemberPaymentMethodStatus.ACTIVE -> PaymentConnection.Active - MemberPaymentMethodStatus.PENDING -> PaymentConnection.Pending - MemberPaymentMethodStatus.UNKNOWN__ -> PaymentConnection.Unknown - } - }, - ) - - - val paymentConnection = run { - val paymentInformation = result.currentMember.paymentInformation - when (paymentInformation.status) { - MemberPaymentConnectionStatus.ACTIVE -> { - PaymentConnection.Active( - displayName = paymentInformation.chargeMethod?.displayName, - displayValue = paymentInformation.chargeMethod?.descriptor, - chargeMethod = paymentInformation.chargeMethod?.paymentMethod.toChargeMethod(), - ) - } - - MemberPaymentConnectionStatus.PENDING -> { - PaymentConnection.Pending - } - - MemberPaymentConnectionStatus.NEEDS_SETUP -> { - val firstKnownTerminationDateForContractTerminatedDueToMissedPayments = result - .currentMember - .activeContracts - .filter { it.terminationDueToMissedPayments } - .mapNotNull { it.terminationDate } - .sorted() - .firstOrNull() - PaymentConnection.NeedsSetup(firstKnownTerminationDateForContractTerminatedDueToMissedPayments) - } - - MemberPaymentConnectionStatus.UNKNOWN__ -> { - PaymentConnection.Unknown - } - } - } - val memberChargeShortInfo = result.currentMember.futureCharge?.toMemberChargeShortInfo() - - val missedChargeIdToChargeManually: String? = result.currentMember.missedChargeIdToChargeManually - - val isManualChargeAllowed = if (missedChargeIdToChargeManually!=null) { - val failedChargeNet = result.currentMember.pastCharges.firstOrNull { - it.id == missedChargeIdToChargeManually}?.net?.let { net -> - UiMoney.fromMoneyFragment(net) - } - if (failedChargeNet!=null) { - ManualChargeToPrompt(failedChargeNet) - } else null - } else { - null - } - - PaymentOverview( - memberChargeShortInfo = memberChargeShortInfo, - ongoingCharges = result.currentMember.ongoingCharges.mapNotNull { - val id = it.id ?: return@mapNotNull null - OngoingCharge(id, it.date, UiMoney.fromMoneyFragment(it.net)) - }, - isManualChargeAllowed = isManualChargeAllowed, - paymentConnection = paymentConnection, - ) - } - }, - ) - delay(3.seconds) + override suspend fun invoke(): Either = either { + val result = apolloClient.query(UpcomingPaymentQuery()) + .fetchPolicy(FetchPolicy.NetworkFirst) + .safeExecute(::ErrorMessage) + .bind() + + val missedChargeIdToChargeManually: String? = result.currentMember.missedChargeIdToChargeManually + + val isManualChargeAllowed = if (missedChargeIdToChargeManually != null) { + val failedChargeNet = result.currentMember.pastCharges.firstOrNull { + it.id == missedChargeIdToChargeManually + }?.net?.let { net -> + UiMoney.fromMoneyFragment(net) } + if (failedChargeNet != null) { + ManualChargeToPrompt(failedChargeNet) + } else null + } else { + null } + + PaymentOverview( + memberChargeShortInfo = result.currentMember.futureCharge?.toMemberChargeShortInfo(), + ongoingCharges = result.currentMember.ongoingCharges.mapNotNull { + val id = it.id ?: return@mapNotNull null + OngoingCharge(id, it.date, UiMoney.fromMoneyFragment(it.net)) + }, + paymentConnection = run { + val paymentMethods = result.currentMember.paymentMethods + val payinMethod = paymentMethods.defaultPayinMethod + ?: paymentMethods.payinMethods.find { it.isDefault } + if (payinMethod == null) { + val firstKnownTerminationDateForContractTerminatedDueToMissedPayments = result + .currentMember + .activeContracts + .filter { it.terminationDueToMissedPayments } + .mapNotNull { it.terminationDate } + .sorted() + .firstOrNull() + return@run PaymentConnection.NeedsSetup( + firstKnownTerminationDateForContractTerminatedDueToMissedPayments, + ) + } + when (payinMethod.status) { + MemberPaymentMethodStatus.ACTIVE -> PaymentConnection.Active + MemberPaymentMethodStatus.PENDING -> PaymentConnection.Pending + MemberPaymentMethodStatus.UNKNOWN__ -> PaymentConnection.Unknown + } + }, + isManualChargeAllowed = isManualChargeAllowed, + ) } } @@ -177,20 +106,18 @@ private fun MemberChargeFragment.toMemberChargeShortInfo() = MemberChargeShortIn internal class GetUpcomingPaymentUseCaseDemo( private val clock: Clock, ) : GetUpcomingPaymentUseCase { - override suspend fun invoke(): Flow> { - return flowOf( - PaymentOverview( - MemberChargeShortInfo( - netAmount = UiMoney(100.0, UiCurrencyCode.SEK), - id = "id", - status = MemberCharge.MemberChargeStatus.SUCCESS, - dueDate = (clock.now() + 10.days).toLocalDateTime(TimeZone.UTC).date, - failedCharge = null, - ), - emptyList(), - PaymentConnection.Unknown, - isManualChargeAllowed = null, - ).right(), - ) + override suspend fun invoke(): Either { + return PaymentOverview( + MemberChargeShortInfo( + netAmount = UiMoney(100.0, UiCurrencyCode.SEK), + id = "id", + status = MemberCharge.MemberChargeStatus.SUCCESS, + dueDate = (clock.now() + 10.days).toLocalDateTime(TimeZone.UTC).date, + failedCharge = null, + ), + emptyList(), + PaymentConnection.Unknown, + isManualChargeAllowed = null, + ).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 b6df1c7bfc..95252355bd 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,8 @@ 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.design.system.hedvig.ButtonDefaults +import com.hedvig.android.design.system.hedvig.HedvigButton import com.hedvig.android.design.system.hedvig.HedvigCard import com.hedvig.android.design.system.hedvig.HedvigErrorSection import com.hedvig.android.design.system.hedvig.HedvigInformationSection 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 400dc28e45..8af364d827 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 @@ -29,6 +29,7 @@ import com.hedvig.android.molecule.public.MoleculePresenterScope import kotlinx.coroutines.flow.collectLatest import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.firstOrNull import kotlinx.datetime.LocalDate internal class PaymentsPresenter( @@ -50,7 +51,7 @@ internal class PaymentsPresenter( LaunchedEffect(loadIteration) { paymentOverviewResult = null - paymentOverviewResult = getUpcomingPaymentUseCase.provide().invoke() + paymentOverviewResult = getUpcomingPaymentUseCase.provide().invoke().firstOrNull() //todo: check } val shouldShowPayout = shouldShowPayoutPresenter.present(loadIteration) From 164e905455efb396ca80e697213163191185bfb9 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 6 May 2026 11:11:57 +0200 Subject: [PATCH 16/29] finish merging --- .../android/feature/payments/ui/payments/PaymentsPresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8af364d827..3e0ede1f47 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 @@ -51,7 +51,7 @@ internal class PaymentsPresenter( LaunchedEffect(loadIteration) { paymentOverviewResult = null - paymentOverviewResult = getUpcomingPaymentUseCase.provide().invoke().firstOrNull() //todo: check + paymentOverviewResult = getUpcomingPaymentUseCase.provide().invoke() } val shouldShowPayout = shouldShowPayoutPresenter.present(loadIteration) From fdb56b56d513918d84d023fc86a8dd50661e3298 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 6 May 2026 12:08:01 +0200 Subject: [PATCH 17/29] add cancellation warning --- .../androidMain/res/values-sv-rSE/strings.xml | 1 + .../src/androidMain/res/values/strings.xml | 1 + .../values-sv-rSE/strings.xml | 1 + .../composeResources/values/strings.xml | 1 + .../design/system/hedvig/EmptyState.kt | 11 + .../design/system/hedvig/Notification.kt | 24 +- .../graphql/QueryManualChargeInfo.graphql | 4 + .../data/GetManualChargeInfoUseCase.kt | 6 + .../navigation/PaymentsDestination.kt | 4 +- .../payments/navigation/PaymentsGraph.kt | 10 +- .../manualcharge/ManualChargeDestination.kt | 258 ++++++++++-------- .../ManualChargeSuccessDestination.kt | 39 ++- .../ui/manualcharge/ManualChargeViewModel.kt | 10 +- 13 files changed, 239 insertions(+), 131 deletions(-) diff --git a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml index 8adea1e591..0de36d7f86 100644 --- a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml @@ -522,6 +522,7 @@ Fråga angående skadeanmälan - Fordon reg. %1$s Välj land och språk Logga ut + För att återaktivera din försäkring efter avslut, kontakta Hedvig Service Team när betalningen har registrerats på ditt konto Få ett prisförslag Se över kontaktuppgifter Se till att vi har rätt kontaktuppgifter ifall vi behöver nå dig. diff --git a/app/core/core-resources/src/androidMain/res/values/strings.xml b/app/core/core-resources/src/androidMain/res/values/strings.xml index 6d855e8fcd..9ea5945eb4 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -522,6 +522,7 @@ Question regarding claim, Vehicle reg. %1$s Preferences Logout + To reactivate your insurance from cancellation, reach out to the Hedvig Service Team after the charge has appeared on your account Get a price quote Review contact info Make sure we have the right contact info in case we need to reach you. diff --git a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml index d5254ac25d..ac53d4e654 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -522,6 +522,7 @@ Fråga angående skadeanmälan - Fordon reg. %1$s Välj land och språk Logga ut + För att återaktivera din försäkring efter avslut, kontakta Hedvig Service Team när betalningen har registrerats på ditt konto Få ett prisförslag Se över kontaktuppgifter Se till att vi har rätt kontaktuppgifter ifall vi behöver nå dig. diff --git a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml index f36a16313e..b9f86e1f5f 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -522,6 +522,7 @@ Question regarding claim, Vehicle reg. %1$s Preferences Logout + To reactivate your insurance from cancellation, reach out to the Hedvig Service Team after the charge has appeared on your account Get a price quote Review contact info Make sure we have the right contact info in case we need to reach you. diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/EmptyState.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/EmptyState.kt index 3d96a407e7..567de7b4fd 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/EmptyState.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/EmptyState.kt @@ -138,6 +138,16 @@ private fun ColumnScope.EmptyStateIcon(iconStyle: EmptyStateIconStyle) { } NO_ICON -> {} + + EmptyStateIconStyle.SUCCESS_WITH_WARNING -> { + Icon( + HedvigIcons.CheckFilled, + null, + tint = emptyStateColors.errorIconColor, + modifier = sizeModifier, + ) + Spacer(Modifier.height(16.dp)) + } } } @@ -151,6 +161,7 @@ object EmptyStateDefaults { SUCCESS, BANK_ID, NO_ICON, + SUCCESS_WITH_WARNING } sealed class EmptyStateButtonStyle { diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt index 3366acd6cb..c4c4c03508 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt @@ -109,7 +109,7 @@ fun HedvigNotificationCard( ) { val padding = if (withIcon) paddingWithIcon else paddingNoIcon val description = when (priority) { - Attention, Error, Info -> stringResource(Res.string.TALKBACK_NOTIFICATION_CARD) + Attention, NotificationPriority.AttentionRound, Error, Info -> stringResource(Res.string.TALKBACK_NOTIFICATION_CARD) Campaign, InfoInline, NeutralToast, FancyInfo -> "" } @@ -292,6 +292,28 @@ object NotificationDefaults { get() = SecondaryAlt } + data object AttentionRound : NotificationPriority { + override val colors: NotificationColors + @Composable + get() = with(HedvigTheme.colorScheme) { + remember(this) { + NotificationColors( + containerColor = fromToken(SignalAmberFill), + borderColor = fromToken(SignalAmberFill), + textColor = fromToken(SignalAmberText), + iconColor = fromToken(SignalAmberElement), + ) + } + } + override val icon: ImageVector + @Composable + get() = + HedvigIcons.InfoFilled + + override val buttonStyle: ButtonDefaults.ButtonStyle + get() = SecondaryAlt + } + data object Error : NotificationPriority { override val colors: NotificationColors @Composable diff --git a/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql b/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql index 1cbc3a7bb6..da34fe73c5 100644 --- a/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql +++ b/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql @@ -19,5 +19,9 @@ query ManualChargeInfo { paymentMethod } } + activeContracts { + terminationDueToMissedPayments + terminationDate + } } } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt index 5e1d097080..e3004e1a38 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt @@ -32,6 +32,10 @@ internal class GetManualChargeInfoUseCaseImpl( val showManualCharge = currentMember.missedChargeIdToChargeManually + val showCancellationWarning = + currentMember.activeContracts + .any { it.terminationDueToMissedPayments && it.terminationDate != null } + if (showManualCharge == null) { logcat { "GetManualChargeInfoUseCaseImpl: missedChargeIdToChargeManually is null" } raise(ErrorMessage()) @@ -51,6 +55,7 @@ internal class GetManualChargeInfoUseCaseImpl( amountDue = UiMoney.fromMoneyFragment(latestFailedPastCharge.net), bankAccountDisplayValue = currentMember.paymentInformation.chargeMethod?.displayName, bankDescriptor = currentMember.paymentInformation.chargeMethod?.descriptor, + showCancellationWarning = showCancellationWarning ) } } @@ -61,4 +66,5 @@ internal data class ManualChargeInfo( val amountDue: UiMoney, val bankDescriptor: String?, val bankAccountDisplayValue: String?, + val showCancellationWarning: Boolean ) diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt index 5dbdbf7dbb..e6a9028f52 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt @@ -33,6 +33,8 @@ internal sealed interface PaymentsDestinations { data object ManualCharge: PaymentsDestinations, Destination @Serializable - data object ManualChargeSuccess: PaymentsDestinations, Destination + data class ManualChargeSuccess( + val showCancellationWarning: Boolean + ): PaymentsDestinations, Destination } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt index 8d40e07281..65319dba97 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt @@ -83,8 +83,10 @@ fun NavGraphBuilder.paymentsGraph( ), ) }, - onNavigateToSuccess = { - navController.navigate(PaymentsDestinations.ManualChargeSuccess) { + onNavigateToSuccess = { showCancellationWarning -> + navController.navigate(PaymentsDestinations.ManualChargeSuccess( + showCancellationWarning = showCancellationWarning + )) { typedPopUpTo { inclusive = true } @@ -94,7 +96,9 @@ fun NavGraphBuilder.paymentsGraph( } navdestination{ - ManualChargeSuccessDestination(navController::navigateUp) + ManualChargeSuccessDestination( + this.showCancellationWarning, + navController::navigateUp) } navdestination { diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt index 6c0041e3f4..a49647dc19 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt @@ -18,8 +18,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.hedvig.android.compose.ui.preview.BooleanCollectionPreviewParameterProvider import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney @@ -27,12 +29,14 @@ import com.hedvig.android.design.system.hedvig.ButtonDefaults 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.HedvigNotificationCard import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigScaffold import com.hedvig.android.design.system.hedvig.HedvigText import com.hedvig.android.design.system.hedvig.HedvigTheme import com.hedvig.android.design.system.hedvig.HorizontalDivider import com.hedvig.android.design.system.hedvig.Icon +import com.hedvig.android.design.system.hedvig.NotificationDefaults import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.design.system.hedvig.hedvigDropShadow import com.hedvig.android.design.system.hedvig.icon.HedvigIcons @@ -42,6 +46,7 @@ import com.hedvig.android.design.system.hedvig.rememberHedvigMonthDateTimeFormat import com.hedvig.android.feature.payments.data.ManualChargeInfo import hedvig.resources.GENERAL_ERROR_BODY import hedvig.resources.GENERAL_RETRY +import hedvig.resources.MANUAL_CHARGE_CANCELLATION_WARNING import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_BODY import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_DUE_DATE import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_FINE_PRINT @@ -60,7 +65,7 @@ internal fun ManualChargeDestination( viewModel: ManualChargeViewModel, navigateUp: () -> Unit, onNavigateToPaymentDetails: (chargeId: String) -> Unit, - onNavigateToSuccess: () -> Unit, + onNavigateToSuccess: (Boolean) -> Unit, ) { val uiState = viewModel.uiState.collectAsStateWithLifecycle() @@ -69,9 +74,9 @@ internal fun ManualChargeDestination( navigateUp = navigateUp, reload = { viewModel.emit(ManualChargeEvent.Retry) }, onNavigateToPaymentDetails = onNavigateToPaymentDetails, - onNavigateToSuccess = { + onNavigateToSuccess = { showCancellationWarning -> viewModel.emit(ManualChargeEvent.ClearNav) - onNavigateToSuccess() + onNavigateToSuccess(showCancellationWarning) }, onTriggerPayment = { viewModel.emit(ManualChargeEvent.TriggerCharge) } @@ -84,7 +89,7 @@ private fun ManualChargeScreen( navigateUp: () -> Unit, reload: () -> Unit, onNavigateToPaymentDetails: (chargeId: String) -> Unit, - onNavigateToSuccess: () -> Unit, + onNavigateToSuccess: (Boolean) -> Unit, onTriggerPayment: () -> Unit ) { HedvigScaffold( @@ -118,7 +123,7 @@ private fun ManualChargeScreen( is ManualChargeUiState.Success -> { if (uiState.navigateToSuccess!=null) { LaunchedEffect(uiState.navigateToSuccess) { - onNavigateToSuccess() + onNavigateToSuccess(uiState.manualChargeInfo.showCancellationWarning) } } else { ManualChargeSuccessScreen( @@ -140,154 +145,170 @@ private fun ManualChargeSuccessScreen( ) { val dateTimeFormatter = rememberHedvigMonthDateTimeFormatter() val dateTimeFormatterWithYear = rememberHedvigDateTimeFormatter() - Column( - modifier = Modifier - .padding( - top = 8.dp, - start = 16.dp, - end = 16.dp, - bottom = 16.dp, - ) - .hedvigDropShadow(HedvigTheme.shapes.cornerXLarge) - .fillMaxWidth() - .background( - color = HedvigTheme.colorScheme.backgroundPrimary, - shape = HedvigTheme.shapes.cornerXLarge, - ) - .border( - width = 1.dp, - color = HedvigTheme.colorScheme.borderPrimary, - shape = HedvigTheme.shapes.cornerXLarge, - ) + Column { - .clip(HedvigTheme.shapes.cornerXLarge) - .padding(16.dp), - ) { - Row( + + Column( modifier = Modifier + .padding( + top = 8.dp, + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ) + .hedvigDropShadow(HedvigTheme.shapes.cornerXLarge) .fillMaxWidth() - .padding(bottom = 16.dp), - verticalAlignment = Alignment.Top, - ) { - Icon( - imageVector = HedvigIcons.WarningFilled, - contentDescription = null, - tint = HedvigTheme.colorScheme.signalRedElement, - modifier = Modifier.size(40.dp), - ) - Spacer(modifier = Modifier.width(12.dp)) - Column(modifier = Modifier.weight(1f)) { - HedvigText( - text = stringResource( - Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_SINCE, - dateTimeFormatter.format(uiState.manualChargeInfo.missedDueDate), - ), + .background( + color = HedvigTheme.colorScheme.backgroundPrimary, + shape = HedvigTheme.shapes.cornerXLarge, ) - HedvigText( - text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_BODY), - color = HedvigTheme.colorScheme.textSecondary, + .border( + width = 1.dp, + color = HedvigTheme.colorScheme.borderPrimary, + shape = HedvigTheme.shapes.cornerXLarge, ) - } - } - if (uiState.manualChargeInfo.chargeId!=null) { - HedvigButton( - text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_VIEW_DETAILS), - onClick = { - onNavigateToPaymentDetails(uiState.manualChargeInfo.chargeId) - }, - enabled = true, - modifier = Modifier.fillMaxWidth(), - buttonStyle = ButtonDefaults.ButtonStyle.Ghost, - buttonSize = ButtonDefaults.ButtonSize.Medium, - border = HedvigTheme.colorScheme.borderPrimary, - ) - } - Spacer(modifier = Modifier.height(16.dp)) - Column( - modifier = Modifier.fillMaxWidth(), + + .clip(HedvigTheme.shapes.cornerXLarge) + .padding(16.dp), ) { Row( modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, + .fillMaxWidth() + .padding(bottom = 16.dp), + verticalAlignment = Alignment.Top, ) { - HedvigText( - text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_DUE_DATE), - color = HedvigTheme.colorScheme.textSecondary, - style = HedvigTheme.typography.label, + Icon( + imageVector = HedvigIcons.WarningFilled, + contentDescription = null, + tint = HedvigTheme.colorScheme.signalRedElement, + modifier = Modifier.size(40.dp), ) - HedvigText( - text = dateTimeFormatterWithYear.format(uiState.manualChargeInfo.missedDueDate), - color = HedvigTheme.colorScheme.textSecondary, - style = HedvigTheme.typography.label, + Spacer(modifier = Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + HedvigText( + text = stringResource( + Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_SINCE, + dateTimeFormatter.format(uiState.manualChargeInfo.missedDueDate), + ), + ) + HedvigText( + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_BODY), + color = HedvigTheme.colorScheme.textSecondary, + ) + } + } + if (uiState.manualChargeInfo.chargeId != null) { + HedvigButton( + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_VIEW_DETAILS), + onClick = { + onNavigateToPaymentDetails(uiState.manualChargeInfo.chargeId) + }, + enabled = true, + modifier = Modifier.fillMaxWidth(), + buttonStyle = ButtonDefaults.ButtonStyle.Ghost, + buttonSize = ButtonDefaults.ButtonSize.Medium, + border = HedvigTheme.colorScheme.borderPrimary, ) } - if (uiState.manualChargeInfo.bankDescriptor!=null && - uiState.manualChargeInfo.bankAccountDisplayValue!=null) { - Spacer(Modifier.height(10.dp)) + Spacer(modifier = Modifier.height(16.dp)) + Column( + modifier = Modifier.fillMaxWidth(), + ) { Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, ) { HedvigText( - text = uiState.manualChargeInfo.bankDescriptor, + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_DUE_DATE), color = HedvigTheme.colorScheme.textSecondary, style = HedvigTheme.typography.label, ) HedvigText( - text = uiState.manualChargeInfo.bankAccountDisplayValue, + text = dateTimeFormatterWithYear.format(uiState.manualChargeInfo.missedDueDate), color = HedvigTheme.colorScheme.textSecondary, style = HedvigTheme.typography.label, ) } + if (uiState.manualChargeInfo.bankDescriptor != null && + uiState.manualChargeInfo.bankAccountDisplayValue != null + ) { + Spacer(Modifier.height(10.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + HedvigText( + text = uiState.manualChargeInfo.bankDescriptor, + color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label, + ) + HedvigText( + text = uiState.manualChargeInfo.bankAccountDisplayValue, + color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label, + ) + } + } } - } - Spacer(modifier = Modifier.height(16.dp)) - HorizontalDivider() - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider() + Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - HedvigText( - text = stringResource(Res.string.payment_details_receipt_card_total), + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + HedvigText( + text = stringResource(Res.string.payment_details_receipt_card_total), + ) + HedvigText( + text = uiState.manualChargeInfo.amountDue.toString(), + textAlign = TextAlign.End, + ) + } + + HedvigButton( + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_PAY, uiState.manualChargeInfo.amountDue), + onClick = onTriggerPayment, + enabled = true, + modifier = Modifier.fillMaxWidth(), ) + + Spacer(modifier = Modifier.height(8.dp)) HedvigText( - text = uiState.manualChargeInfo.amountDue.toString(), - textAlign = TextAlign.End, + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_FINE_PRINT), + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + textAlign = TextAlign.Center, + style = HedvigTheme.typography.label, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), ) } - - HedvigButton( - text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_PAY, uiState.manualChargeInfo.amountDue), - onClick = onTriggerPayment, - enabled = true, - modifier = Modifier.fillMaxWidth(), - ) - - Spacer(modifier = Modifier.height(8.dp)) - HedvigText( - text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_FINE_PRINT), - color = HedvigTheme.colorScheme.textSecondaryTranslucent, - textAlign = TextAlign.Center, - style = HedvigTheme.typography.label, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) + if (uiState.manualChargeInfo.showCancellationWarning) { + HedvigNotificationCard( + message = stringResource(Res.string.MANUAL_CHARGE_CANCELLATION_WARNING), + priority = NotificationDefaults.NotificationPriority.AttentionRound, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + ) + Spacer(Modifier.height(16.dp)) + } } - } @Composable @Preview @HedvigPreview -private fun ManualChargeScreenSuccessPreview() { +private fun ManualChargeScreenSuccessPreview( + @PreviewParameter( + BooleanCollectionPreviewParameterProvider::class, + ) showCancellationWarning: Boolean, +) { HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { ManualChargeScreen( @@ -297,9 +318,10 @@ private fun ManualChargeScreenSuccessPreview() { amountDue = UiMoney(100.0, UiCurrencyCode.SEK), chargeId = "chargeId", bankDescriptor = "Bank account", - bankAccountDisplayValue = "**** 8324" + bankAccountDisplayValue = "**** 8324", + showCancellationWarning = showCancellationWarning ), - navigateToSuccess = null + navigateToSuccess = null, ), navigateUp = {}, reload = {}, diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt index 05ad0426c0..c9d510c9ad 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt @@ -12,16 +12,21 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.dropUnlessResumed +import com.hedvig.android.compose.ui.preview.BooleanCollectionPreviewParameterProvider import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize.Large import com.hedvig.android.design.system.hedvig.EmptyState +import com.hedvig.android.design.system.hedvig.EmptyStateDefaults import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateButtonStyle.NoButton import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateIconStyle.SUCCESS +import com.hedvig.android.design.system.hedvig.HedvigNotificationCard import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigTextButton +import com.hedvig.android.design.system.hedvig.NotificationDefaults +import hedvig.resources.MANUAL_CHARGE_CANCELLATION_WARNING import hedvig.resources.PAYMENTS_PAYMENT_IN_PROGRESS import hedvig.resources.PAYMENTS_PAYMENT_IN_PROGRESS_DESCRIPTION import hedvig.resources.Res @@ -29,7 +34,10 @@ import hedvig.resources.general_close_button import org.jetbrains.compose.resources.stringResource @Composable -internal fun ManualChargeSuccessDestination(popBackStack: () -> Unit) { +internal fun ManualChargeSuccessDestination( + showCancellationWarning: Boolean, + popBackStack: () -> Unit, +) { Column( modifier = Modifier .fillMaxSize() @@ -48,10 +56,22 @@ internal fun ManualChargeSuccessDestination(popBackStack: () -> Unit) { description = stringResource( Res.string.PAYMENTS_PAYMENT_IN_PROGRESS_DESCRIPTION, ), - iconStyle = SUCCESS, + iconStyle = if (showCancellationWarning) + EmptyStateDefaults.EmptyStateIconStyle.SUCCESS_WITH_WARNING + else SUCCESS, buttonStyle = NoButton, ) - Spacer(Modifier.weight(1f)) + Column(Modifier.weight(1f)) { + if (showCancellationWarning) { + Spacer(Modifier.height(32.dp)) + HedvigNotificationCard( + message = stringResource(Res.string.MANUAL_CHARGE_CANCELLATION_WARNING), + priority = NotificationDefaults.NotificationPriority.AttentionRound, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(16.dp)) + } + } HedvigTextButton( stringResource(Res.string.general_close_button), onClick = dropUnlessResumed { popBackStack() }, @@ -64,6 +84,13 @@ internal fun ManualChargeSuccessDestination(popBackStack: () -> Unit) { @HedvigPreview @Composable -private fun ManualChargeSuccessDestinationPreview() { - ManualChargeSuccessDestination({}) +private fun ManualChargeSuccessDestinationPreview( + @PreviewParameter( + BooleanCollectionPreviewParameterProvider::class, + ) showCancellationWarning: Boolean, +) { + ManualChargeSuccessDestination( + showCancellationWarning, + {}, + ) } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt index 1d2244377b..ecdf185baa 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt @@ -56,7 +56,10 @@ private class ManualChargePresenter( screenState = ManualChargeUiState.Failure(it) }, ifRight = { - screenState = ManualChargeUiState.Success(currentState.manualChargeInfo, Unit) + screenState = ManualChargeUiState.Success( + manualChargeInfo = currentState.manualChargeInfo, + navigateToSuccess = Unit + ) } ) } @@ -66,7 +69,10 @@ private class ManualChargePresenter( screenState = ManualChargeUiState.Loading getManualChargeInfoUseCase.invoke().fold( ifRight = { manualChargeInfo -> - screenState = ManualChargeUiState.Success(manualChargeInfo, null) + screenState = ManualChargeUiState.Success( + manualChargeInfo = manualChargeInfo, + null, + ) }, ifLeft = { failure -> screenState = ManualChargeUiState.Failure(failure) From e707598c16612d247d7b5e6ece3a64cf7809c4bf Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 6 May 2026 12:55:42 +0200 Subject: [PATCH 18/29] fix previews --- .../manualcharge/ManualChargeDestination.kt | 2 +- .../ManualChargeSuccessDestination.kt | 22 ++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt index a49647dc19..513a75b684 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt @@ -338,7 +338,7 @@ private fun ManualChargeScreenSuccessPreview( @HedvigPreview private fun ManualChargeScreenLoadingPreview() { HedvigTheme { - Surface { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { ManualChargeScreen( uiState = ManualChargeUiState.Loading, navigateUp = {}, diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt index c9d510c9ad..75aae2975b 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt @@ -17,15 +17,19 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.dropUnlessResumed import com.hedvig.android.compose.ui.preview.BooleanCollectionPreviewParameterProvider +import com.hedvig.android.design.system.hedvig.ButtonDefaults import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize.Large import com.hedvig.android.design.system.hedvig.EmptyState import com.hedvig.android.design.system.hedvig.EmptyStateDefaults import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateButtonStyle.NoButton import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateIconStyle.SUCCESS +import com.hedvig.android.design.system.hedvig.HedvigButton import com.hedvig.android.design.system.hedvig.HedvigNotificationCard import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigTextButton +import com.hedvig.android.design.system.hedvig.HedvigTheme import com.hedvig.android.design.system.hedvig.NotificationDefaults +import com.hedvig.android.design.system.hedvig.Surface import hedvig.resources.MANUAL_CHARGE_CANCELLATION_WARNING import hedvig.resources.PAYMENTS_PAYMENT_IN_PROGRESS import hedvig.resources.PAYMENTS_PAYMENT_IN_PROGRESS_DESCRIPTION @@ -63,7 +67,7 @@ internal fun ManualChargeSuccessDestination( ) Column(Modifier.weight(1f)) { if (showCancellationWarning) { - Spacer(Modifier.height(32.dp)) + Spacer(Modifier.height(16.dp)) HedvigNotificationCard( message = stringResource(Res.string.MANUAL_CHARGE_CANCELLATION_WARNING), priority = NotificationDefaults.NotificationPriority.AttentionRound, @@ -72,11 +76,13 @@ internal fun ManualChargeSuccessDestination( Spacer(Modifier.height(16.dp)) } } - HedvigTextButton( + HedvigButton( stringResource(Res.string.general_close_button), onClick = dropUnlessResumed { popBackStack() }, buttonSize = Large, modifier = Modifier.fillMaxWidth(), + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Secondary ) Spacer(Modifier.height(16.dp)) } @@ -89,8 +95,12 @@ private fun ManualChargeSuccessDestinationPreview( BooleanCollectionPreviewParameterProvider::class, ) showCancellationWarning: Boolean, ) { - ManualChargeSuccessDestination( - showCancellationWarning, - {}, - ) + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + ManualChargeSuccessDestination( + showCancellationWarning, + {}, + ) + } + } } From 69394e3cb7b3f76bebf6aca8fb73687de23146d4 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 6 May 2026 13:04:42 +0200 Subject: [PATCH 19/29] fix no ongoing text --- .../ui/payments/PaymentsDestination.kt | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) 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 95252355bd..915c794056 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 @@ -276,12 +276,14 @@ private fun PaymentsContent( } val upcomingPayment = (uiState as? Content)?.upcomingPayment if (upcomingPayment == NoUpcomingPayment) { - HedvigInformationSection( - stringResource(Res.string.PAYMENTS_NO_PAYMENTS_IN_PROGRESS), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) + if (ongoingCharges.isNullOrEmpty()) { + HedvigInformationSection( + stringResource(Res.string.PAYMENTS_NO_PAYMENTS_IN_PROGRESS), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } } else { PaymentAmountCard( upcomingPayment = upcomingPayment as? UpcomingPayment.Content, @@ -800,7 +802,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< Content( isRetrying = false, upcomingPayment = UpcomingPayment.Content( - UiMoney(100.0, SEK), + UiMoney(400.0, SEK), System.now().toLocalDateTime(TimeZone.UTC).date, "pwe", ), @@ -841,9 +843,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< upcomingPaymentInfo = PaymentFailed( System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, - isManualChargeAllowed = ManualChargeToPrompt( - UiMoney(200.0, UiCurrencyCode.SEK), - ), + isManualChargeAllowed = null, ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( @@ -879,9 +879,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< upcomingPaymentInfo = PaymentFailed( System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, - isManualChargeAllowed = ManualChargeToPrompt( - UiMoney(200.0, UiCurrencyCode.SEK), - ), + isManualChargeAllowed = null, ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( @@ -901,9 +899,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< upcomingPaymentInfo = PaymentFailed( System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, - isManualChargeAllowed = ManualChargeToPrompt( - UiMoney(200.0, UiCurrencyCode.SEK), - ), + isManualChargeAllowed = null, ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( From 4d52c77d59602dc3e9b3d8259c659c4426d3d4ba Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 6 May 2026 13:12:48 +0200 Subject: [PATCH 20/29] change userError message and button --- .../android/app/navigation/HedvigNavHost.kt | 3 +++ .../payments/navigation/PaymentsGraph.kt | 4 +++- .../manualcharge/ManualChargeDestination.kt | 23 +++++++++++++++---- 3 files changed, 24 insertions(+), 6 deletions(-) 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 96db5c7f39..45d55c627a 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 @@ -346,6 +346,9 @@ internal fun HedvigNavHost( navigateToPayoutAccount = { navController.navigate(PayoutAccountDestination.Graph) }, languageService = languageService, hedvigBuildConstants = hedvigBuildConstants, + openConversation = { + navigateToNewConversation() + } ) payoutAccountGraph( navController = navController, diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt index 65319dba97..08804dd98b 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt @@ -37,6 +37,7 @@ fun NavGraphBuilder.paymentsGraph( hedvigBuildConstants: HedvigBuildConstants, navigateToConnectPayment: () -> Unit, navigateToPayoutAccount: () -> Unit, + openConversation: () -> Unit, ) { navgraph( startDestination = PaymentsDestination.Payments::class, @@ -91,7 +92,8 @@ fun NavGraphBuilder.paymentsGraph( inclusive = true } } - } + }, + openConversation = openConversation ) } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt index 513a75b684..5f81e303ea 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt @@ -55,6 +55,7 @@ import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_SINCE import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_VIEW_DETAILS import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_TITLE import hedvig.resources.Res +import hedvig.resources.claim_status_detail_chat_button_description import hedvig.resources.general_close_button import hedvig.resources.payment_details_receipt_card_total import kotlinx.datetime.LocalDate @@ -66,6 +67,7 @@ internal fun ManualChargeDestination( navigateUp: () -> Unit, onNavigateToPaymentDetails: (chargeId: String) -> Unit, onNavigateToSuccess: (Boolean) -> Unit, + openConversation: ()-> Unit, ) { val uiState = viewModel.uiState.collectAsStateWithLifecycle() @@ -79,7 +81,8 @@ internal fun ManualChargeDestination( onNavigateToSuccess(showCancellationWarning) }, onTriggerPayment = { - viewModel.emit(ManualChargeEvent.TriggerCharge) } + viewModel.emit(ManualChargeEvent.TriggerCharge) }, + openConversation = openConversation ) } @@ -88,6 +91,7 @@ private fun ManualChargeScreen( uiState: ManualChargeUiState, navigateUp: () -> Unit, reload: () -> Unit, + openConversation: ()-> Unit, onNavigateToPaymentDetails: (chargeId: String) -> Unit, onNavigateToSuccess: (Boolean) -> Unit, onTriggerPayment: () -> Unit @@ -101,9 +105,9 @@ private fun ManualChargeScreen( is ManualChargeUiState.Failure -> { val subTitle = if (uiState.error.message!=null) uiState.error.message else stringResource(Res.string.GENERAL_ERROR_BODY) - val buttonText = if (uiState.error.message!=null) stringResource(Res.string.general_close_button) else + val buttonText = if (uiState.error.message!=null) stringResource(Res.string.claim_status_detail_chat_button_description) else stringResource(Res.string.GENERAL_RETRY) - val onButtonClick = if (uiState.error.message!=null) navigateUp else reload + val onButtonClick = if (uiState.error.message!=null) openConversation else reload HedvigErrorSection( onButtonClick = onButtonClick, @@ -328,6 +332,7 @@ private fun ManualChargeScreenSuccessPreview( {}, {}, {}, + {} ) } } @@ -346,6 +351,7 @@ private fun ManualChargeScreenLoadingPreview() { {}, {}, {}, + {} ) } } @@ -354,16 +360,23 @@ private fun ManualChargeScreenLoadingPreview() { @Composable @Preview @HedvigPreview -private fun ManualChargeScreenFailurePreview() { +private fun ManualChargeScreenFailurePreview( + @PreviewParameter( + BooleanCollectionPreviewParameterProvider::class, + ) hasUserError: Boolean, +) { HedvigTheme { Surface { ManualChargeScreen( - uiState = ManualChargeUiState.Failure(ErrorMessage("Payment method not allowed")), + uiState = ManualChargeUiState.Failure(ErrorMessage( + message = if (hasUserError) "Cannot charge the failed payment since there have been some changes. " + + "The new amount will be included in the upcoming payment." else null )), navigateUp = {}, reload = {}, {}, {}, {}, + {} ) } } From 904ab3041f23fdb9ebc29b12433f2d20288c738a Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 8 May 2026 09:35:30 +0200 Subject: [PATCH 21/29] publ to staging --- .github/workflows/staging.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index d0778a65ac..41bbee29b1 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -3,6 +3,7 @@ on: push: branches: - develop + - feat/manual-charge workflow_dispatch: concurrency: From c04d288cfb273e672705d3a67e6938fd6bc09f11 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 8 May 2026 15:03:09 +0200 Subject: [PATCH 22/29] change manual card display items --- .../src/androidMain/res/values-sv-rSE/strings.xml | 8 ++++---- .../src/androidMain/res/values/strings.xml | 8 ++++---- .../composeResources/values-sv-rSE/strings.xml | 8 ++++---- .../commonMain/composeResources/values/strings.xml | 8 ++++---- .../ui/manualcharge/ManualChargeDestination.kt | 13 +++++++------ 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml index 0de36d7f86..094a969c7f 100644 --- a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml @@ -522,7 +522,7 @@ Fråga angående skadeanmälan - Fordon reg. %1$s Välj land och språk Logga ut - För att återaktivera din försäkring efter avslut, kontakta Hedvig Service Team när betalningen har registrerats på ditt konto + Aktivera ditt försäkringsskydd igen genom att kontakta oss när din betalning har registrerats Få ett prisförslag Se över kontaktuppgifter Se till att vi har rätt kontaktuppgifter ifall vi behöver nå dig. @@ -1028,9 +1028,9 @@ Din skadeanmälan granskas av en av våra försäkringsspecialister. Vi hör av oss snart med en uppdatering. Vi har återöppnat din skadeanmälan och en av våra försäkringsspecialister granskar den. Vi hör av oss snart med en uppdatering. Se detaljer - Reg nr + Registreringsnummer Ärendenummer - E-mail + Mail för ärende Saknas något? Skicka ett meddelande till oss här i appen. Inskickad Detaljer om skadeanmälan @@ -1040,7 +1040,7 @@ Din skada täcks, men tyvärr är ersättningen lika stor eller mindre än självrisken. Se konversation för mer information. Tyvärr täcks inte din skada. Se konversation för mer information om beslutet. Din skada har täckts. Du borde ha fått ersättningen utbetald vid det här laget. - Den här skadan hanteras av vår externa partner Eir via mail.\nRedan i kontakt med dem? Svara i din befintliga mailtråd så att all skadeinformation hålls samlad i en och samma konversation. + Våra bilspecialister på Eir hanterar din skadeanmälan via mail.\n\nNär de har gått igenom ärendet får du en bekräftelse via mail. Behöver du komma i kontakt kring din anmälan, svara i samma e-posttråd. Vi har tagit emot din skadeanmälan och kommer snart att börja granska den. Ladda upp foton, kvitton eller andra dokument kopplade till din skadeanmälan Besvärshänvisning diff --git a/app/core/core-resources/src/androidMain/res/values/strings.xml b/app/core/core-resources/src/androidMain/res/values/strings.xml index 9ea5945eb4..b05f78af91 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -522,7 +522,7 @@ Question regarding claim, Vehicle reg. %1$s Preferences Logout - To reactivate your insurance from cancellation, reach out to the Hedvig Service Team after the charge has appeared on your account + Activate your coverage again by contacting us once your payment has been processed Get a price quote Review contact info Make sure we have the right contact info in case we need to reach you. @@ -1028,9 +1028,9 @@ Your claim is being reviewed by one of our insurance specialists. We\'ll get back to you soon with an update. We have reopened your claim and one of our insurance specialists is reviewing it. We\'ll get back to you soon with an update. Show details - Reg. no. + Registration number Reference number - Email + Claim email Is something missing? Send us a message here in the app. Submitted Claim details @@ -1040,7 +1040,7 @@ Your claim was covered but unfortunately the compensation is equal or less than the deductible. Please see conversation for more details. Your claim was unfortunately not covered. Please see conversation for more details on the decision. We got you covered. You should have received the payment by now. - This claim is handled by our external partner Eir by email.\nAlready in contact with them? Reply to your existing email thread to keep all claim details in one conversation. + Our car specialists at Eir handle this claim by email.\n\nYou’ll receive a confirmation by email once it’s been reviewed. If you need to get in touch about your claim, reply in the same email thread. We have received your claim and will start reviewing it soon. Upload photos, receipts or other documents connected to your claim Appeal Instruction (SWE) diff --git a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml index ac53d4e654..dc2ef747b3 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -522,7 +522,7 @@ Fråga angående skadeanmälan - Fordon reg. %1$s Välj land och språk Logga ut - För att återaktivera din försäkring efter avslut, kontakta Hedvig Service Team när betalningen har registrerats på ditt konto + Aktivera ditt försäkringsskydd igen genom att kontakta oss när din betalning har registrerats Få ett prisförslag Se över kontaktuppgifter Se till att vi har rätt kontaktuppgifter ifall vi behöver nå dig. @@ -1028,9 +1028,9 @@ Din skadeanmälan granskas av en av våra försäkringsspecialister. Vi hör av oss snart med en uppdatering. Vi har återöppnat din skadeanmälan och en av våra försäkringsspecialister granskar den. Vi hör av oss snart med en uppdatering. Se detaljer - Reg nr + Registreringsnummer Ärendenummer - E-mail + Mail för ärende Saknas något? Skicka ett meddelande till oss här i appen. Inskickad Detaljer om skadeanmälan @@ -1040,7 +1040,7 @@ Din skada täcks, men tyvärr är ersättningen lika stor eller mindre än självrisken. Se konversation för mer information. Tyvärr täcks inte din skada. Se konversation för mer information om beslutet. Din skada har täckts. Du borde ha fått ersättningen utbetald vid det här laget. - Den här skadan hanteras av vår externa partner Eir via mail.\nRedan i kontakt med dem? Svara i din befintliga mailtråd så att all skadeinformation hålls samlad i en och samma konversation. + Våra bilspecialister på Eir hanterar din skadeanmälan via mail.\n\nNär de har gått igenom ärendet får du en bekräftelse via mail. Behöver du komma i kontakt kring din anmälan, svara i samma e-posttråd. Vi har tagit emot din skadeanmälan och kommer snart att börja granska den. Ladda upp foton, kvitton eller andra dokument kopplade till din skadeanmälan Besvärshänvisning diff --git a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml index b9f86e1f5f..58db1a63d0 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -522,7 +522,7 @@ Question regarding claim, Vehicle reg. %1$s Preferences Logout - To reactivate your insurance from cancellation, reach out to the Hedvig Service Team after the charge has appeared on your account + Activate your coverage again by contacting us once your payment has been processed Get a price quote Review contact info Make sure we have the right contact info in case we need to reach you. @@ -1028,9 +1028,9 @@ Your claim is being reviewed by one of our insurance specialists. We'll get back to you soon with an update. We have reopened your claim and one of our insurance specialists is reviewing it. We'll get back to you soon with an update. Show details - Reg. no. + Registration number Reference number - Email + Claim email Is something missing? Send us a message here in the app. Submitted Claim details @@ -1040,7 +1040,7 @@ Your claim was covered but unfortunately the compensation is equal or less than the deductible. Please see conversation for more details. Your claim was unfortunately not covered. Please see conversation for more details on the decision. We got you covered. You should have received the payment by now. - This claim is handled by our external partner Eir by email.\nAlready in contact with them? Reply to your existing email thread to keep all claim details in one conversation. + Our car specialists at Eir handle this claim by email.\n\nYou’ll receive a confirmation by email once it’s been reviewed. If you need to get in touch about your claim, reply in the same email thread. We have received your claim and will start reviewing it soon. Upload photos, receipts or other documents connected to your claim Appeal Instruction (SWE) diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt index 5f81e303ea..c99321007c 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt @@ -44,6 +44,7 @@ import com.hedvig.android.design.system.hedvig.icon.WarningFilled import com.hedvig.android.design.system.hedvig.rememberHedvigDateTimeFormatter import com.hedvig.android.design.system.hedvig.rememberHedvigMonthDateTimeFormatter import com.hedvig.android.feature.payments.data.ManualChargeInfo +import hedvig.resources.BANK_PAYOUT_METHOD_CARD_TITLE import hedvig.resources.GENERAL_ERROR_BODY import hedvig.resources.GENERAL_RETRY import hedvig.resources.MANUAL_CHARGE_CANCELLATION_WARNING @@ -234,8 +235,7 @@ private fun ManualChargeSuccessScreen( style = HedvigTheme.typography.label, ) } - if (uiState.manualChargeInfo.bankDescriptor != null && - uiState.manualChargeInfo.bankAccountDisplayValue != null + if (uiState.manualChargeInfo.bankDescriptor != null ) { Spacer(Modifier.height(10.dp)) Row( @@ -243,12 +243,12 @@ private fun ManualChargeSuccessScreen( horizontalArrangement = Arrangement.SpaceBetween, ) { HedvigText( - text = uiState.manualChargeInfo.bankDescriptor, + text = stringResource(Res.string.BANK_PAYOUT_METHOD_CARD_TITLE), color = HedvigTheme.colorScheme.textSecondary, style = HedvigTheme.typography.label, ) HedvigText( - text = uiState.manualChargeInfo.bankAccountDisplayValue, + text = uiState.manualChargeInfo.bankDescriptor, color = HedvigTheme.colorScheme.textSecondary, style = HedvigTheme.typography.label, ) @@ -280,6 +280,7 @@ private fun ManualChargeSuccessScreen( text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_PAY, uiState.manualChargeInfo.amountDue), onClick = onTriggerPayment, enabled = true, + buttonSize = ButtonDefaults.ButtonSize.Medium, modifier = Modifier.fillMaxWidth(), ) @@ -321,8 +322,8 @@ private fun ManualChargeScreenSuccessPreview( missedDueDate = LocalDate(2026, 1, 15), amountDue = UiMoney(100.0, UiCurrencyCode.SEK), chargeId = "chargeId", - bankDescriptor = "Bank account", - bankAccountDisplayValue = "**** 8324", + bankDescriptor = "**** 8324", + bankAccountDisplayValue = "Swedbank", showCancellationWarning = showCancellationWarning ), navigateToSuccess = null, From b21d0a62c7cb0f287832714157f1a3ee7e92280a Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 8 May 2026 15:58:27 +0200 Subject: [PATCH 23/29] add feature flag back --- app/feature/feature-payments/build.gradle.kts | 59 ++++++++++--------- .../data/GetManualChargeInfoUseCase.kt | 12 ++++ .../flags/UnleashFeatureFlagProvider.kt | 2 + .../android/featureflags/flags/Feature.kt | 2 + .../data/payment/GetIfMissedPaymentUseCase.kt | 14 ++++- 5 files changed, 59 insertions(+), 30 deletions(-) diff --git a/app/feature/feature-payments/build.gradle.kts b/app/feature/feature-payments/build.gradle.kts index 0a249e7b59..427d2fca10 100644 --- a/app/feature/feature-payments/build.gradle.kts +++ b/app/feature/feature-payments/build.gradle.kts @@ -10,33 +10,34 @@ hedvig { } dependencies { - implementation(libs.apollo.normalizedCache) - implementation(libs.apollo.runtime) - implementation(libs.arrow.core) - implementation(libs.arrow.fx) - implementation(libs.jetbrains.compose.foundation) - implementation(libs.jetbrains.compose.runtime) - implementation(libs.jetbrains.lifecycle.runtime.compose) - implementation(libs.jetbrains.navigation.compose) - implementation(libs.koin.composeViewModel) - implementation(libs.koin.core) - implementation(libs.kotlinx.serialization.core) - implementation(projects.apolloCore) - implementation(projects.apolloOctopusPublic) - implementation(projects.composeUi) - implementation(projects.coreBuildConstants) - implementation(projects.coreCommonPublic) - implementation(projects.coreDemoMode) - implementation(projects.coreResources) - implementation(projects.coreUiData) - implementation(projects.dataPayingMember) - implementation(projects.designSystemHedvig) - implementation(projects.foreverUi) - implementation(projects.languageCore) - implementation(projects.moleculePublic) - implementation(projects.navigationCommon) - implementation(projects.navigationCompose) - implementation(projects.navigationCore) - implementation(projects.pullrefresh) - implementation(projects.theme) + implementation(libs.apollo.normalizedCache) + implementation(libs.apollo.runtime) + implementation(libs.arrow.core) + implementation(libs.arrow.fx) + implementation(libs.jetbrains.compose.foundation) + implementation(libs.jetbrains.compose.runtime) + implementation(libs.jetbrains.lifecycle.runtime.compose) + implementation(libs.jetbrains.navigation.compose) + implementation(libs.koin.composeViewModel) + implementation(libs.koin.core) + implementation(libs.kotlinx.serialization.core) + implementation(projects.apolloCore) + implementation(projects.apolloOctopusPublic) + implementation(projects.composeUi) + implementation(projects.coreBuildConstants) + implementation(projects.coreCommonPublic) + implementation(projects.coreDemoMode) + implementation(projects.coreResources) + implementation(projects.coreUiData) + implementation(projects.dataPayingMember) + implementation(projects.designSystemHedvig) + implementation(projects.featureFlagsPublic) + implementation(projects.foreverUi) + implementation(projects.languageCore) + implementation(projects.moleculePublic) + implementation(projects.navigationCommon) + implementation(projects.navigationCompose) + implementation(projects.navigationCore) + implementation(projects.pullrefresh) + implementation(projects.theme) } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt index e3004e1a38..77dfa2fef7 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt @@ -11,7 +11,11 @@ import com.hedvig.android.apollo.ErrorMessage import com.hedvig.android.apollo.safeExecute import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.logger.logcat +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.datetime.LocalDate import octopus.ManualChargeInfoQuery @@ -21,9 +25,17 @@ internal interface GetManualChargeInfoUseCase { internal class GetManualChargeInfoUseCaseImpl( private val apolloClient: ApolloClient, + private val featureManager: FeatureManager, ) : GetManualChargeInfoUseCase { override suspend fun invoke(): Either = either { + val isFeatureFlagOn = featureManager.isFeatureEnabled(Feature.ENABLE_MANUAL_CHARGE).firstOrNull() ?: false + + if (!isFeatureFlagOn) { + logcat { "ENABLE_MANUAL_CHARGE flag is off" } + raise(ErrorMessage()) + } + val currentMember = apolloClient.query(ManualChargeInfoQuery()) .fetchPolicy(NetworkFirst) .safeExecute(::ErrorMessage) diff --git a/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt b/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt index 0040e59b9a..59aa1c36cf 100644 --- a/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt +++ b/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt @@ -37,6 +37,8 @@ internal class UnleashFeatureFlagProvider( Feature.DISABLE_REDEEM_CAMPAIGN -> hedvigUnleashClient.client.isEnabled("disable_redeem_campaign", false) Feature.ENABLE_CLAIM_HISTORY -> hedvigUnleashClient.client.isEnabled("enable_claim_history", false) + + Feature.ENABLE_MANUAL_CHARGE -> hedvigUnleashClient.client.isEnabled("enable_manual_charge") } }.distinctUntilChanged() } 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 2db61ba0e0..54ec60fbbe 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 @@ -22,4 +22,6 @@ enum class Feature( ), DISABLE_REDEEM_CAMPAIGN("Disables the ability to redeem a campaign code"), ENABLE_CLAIM_HISTORY("Enables claim history"), + + ENABLE_MANUAL_CHARGE("Enables manual self-charge for member in the app payments") } diff --git a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetIfMissedPaymentUseCase.kt b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetIfMissedPaymentUseCase.kt index bf69eb6738..686d730ef1 100644 --- a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetIfMissedPaymentUseCase.kt +++ b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetIfMissedPaymentUseCase.kt @@ -1,16 +1,20 @@ package com.hedvig.android.notification.badge.data.payment +import arrow.core.raise.context.raise import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.cache.normalized.FetchPolicy import com.apollographql.apollo.cache.normalized.fetchPolicy import com.hedvig.android.apollo.safeFlow import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.logger.logcat import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.isActive @@ -22,9 +26,17 @@ interface GetIfMissedPaymentUseCase { internal class GetIfMissedPaymentUseCaseImpl( private val apolloClient: ApolloClient, + private val featureManager: FeatureManager ) : GetIfMissedPaymentUseCase { override fun invoke(): Flow { return flow { + val isFeatureFlagOn = featureManager.isFeatureEnabled(Feature.ENABLE_MANUAL_CHARGE).firstOrNull() ?: false + if (!isFeatureFlagOn) { + logcat { "ENABLE_MANUAL_CHARGE flag is off" } + emit(false) + return@flow + } + while (currentCoroutineContext().isActive) { emitAll( apolloClient @@ -46,7 +58,7 @@ internal class GetIfMissedPaymentUseCaseImpl( ) }, ) - delay(5.seconds) + delay(15.seconds) } } } From 733012af4bece9c11db59bd4eafa95978067e6c4 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 8 May 2026 16:30:08 +0200 Subject: [PATCH 24/29] missing imports and strings --- .../androidMain/res/values-sv-rSE/strings.xml | 3 +- .../src/androidMain/res/values/strings.xml | 3 +- .../values-sv-rSE/strings.xml | 3 +- .../composeResources/values/strings.xml | 3 +- .../feature/payments/di/PaymentsModule.kt | 2 + .../manualcharge/ManualChargeDestination.kt | 58 ++++++++++++------- .../badge/data/di/NotificationBadgeModule.kt | 3 +- 7 files changed, 48 insertions(+), 27 deletions(-) diff --git a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml index 094a969c7f..1121f472a0 100644 --- a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml @@ -610,7 +610,7 @@ Denna betalning misslyckades och lades till i din betalning den %1$s . Betalningshistorik Betalning pågår - Det kan ta upp till 5 bankdagar innan betalningen syns + Det kan ta upp till 5 bankdagar innan betalningen registreras Betalningssätt Att betala: %1$s Vi kunde inte dra betalningen från ditt bankkonto. Betala för att undvika avbrott i ditt skydd. @@ -712,6 +712,7 @@ Sök Välj den försäkring du vill uppdatera Select insurance + Betalningen kan inte genomföras Språk Logga in Notiser diff --git a/app/core/core-resources/src/androidMain/res/values/strings.xml b/app/core/core-resources/src/androidMain/res/values/strings.xml index b05f78af91..ab249a9e6d 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -610,7 +610,7 @@ This payment failed and was added to your payment on %1$s. Payment history Payment in progress - It may take up to 5 business days for the charge to appear + It may take up to 5 business days for the charge to process Payment method Amount due: %1$s We couldn\'t collect this payment from your bank account. Pay now to keep your coverage active. @@ -712,6 +712,7 @@ Search Select the insurance you want to edit Select insurance + Payment can\'t be charged Language Login Notifications diff --git a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml index dc2ef747b3..ee88808b69 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -610,7 +610,7 @@ Denna betalning misslyckades och lades till i din betalning den %1$s . Betalningshistorik Betalning pågår - Det kan ta upp till 5 bankdagar innan betalningen syns + Det kan ta upp till 5 bankdagar innan betalningen registreras Betalningssätt Att betala: %1$s Vi kunde inte dra betalningen från ditt bankkonto. Betala för att undvika avbrott i ditt skydd. @@ -712,6 +712,7 @@ Sök Välj den försäkring du vill uppdatera Select insurance + Betalningen kan inte genomföras Språk Logga in Notiser diff --git a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml index 58db1a63d0..ff57e81a6a 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -610,7 +610,7 @@ This payment failed and was added to your payment on %1$s. Payment history Payment in progress - It may take up to 5 business days for the charge to appear + It may take up to 5 business days for the charge to process Payment method Amount due: %1$s We couldn't collect this payment from your bank account. Pay now to keep your coverage active. @@ -712,6 +712,7 @@ Search Select the insurance you want to edit Select insurance + Payment can't be charged Language Login Notifications diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt index c50b419ca6..c85fc99ab5 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt @@ -33,6 +33,7 @@ import com.hedvig.android.feature.payments.ui.history.PaymentHistoryViewModel import com.hedvig.android.feature.payments.ui.manualcharge.ManualChargeViewModel import com.hedvig.android.feature.payments.ui.memberpaymentdetails.MemberPaymentDetailsViewModel import com.hedvig.android.feature.payments.ui.payments.PaymentsViewModel +import com.hedvig.android.featureflags.FeatureManager import kotlin.time.Clock import org.koin.core.module.dsl.viewModel import org.koin.dsl.module @@ -134,6 +135,7 @@ val paymentsModule = module { single { GetManualChargeInfoUseCaseImpl( get(), + get() ) } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt index c99321007c..88272f65e2 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt @@ -56,9 +56,10 @@ import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_SINCE import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_DETAILS_VIEW_DETAILS import hedvig.resources.PAYMENTS_PAYMENT_OVERDUE_TITLE import hedvig.resources.Res +import hedvig.resources.SELF_MANUAL_CHARGE_CHANGES_BEEN_MADE_TITLE import hedvig.resources.claim_status_detail_chat_button_description -import hedvig.resources.general_close_button import hedvig.resources.payment_details_receipt_card_total +import hedvig.resources.something_went_wrong import kotlinx.datetime.LocalDate import org.jetbrains.compose.resources.stringResource @@ -68,7 +69,7 @@ internal fun ManualChargeDestination( navigateUp: () -> Unit, onNavigateToPaymentDetails: (chargeId: String) -> Unit, onNavigateToSuccess: (Boolean) -> Unit, - openConversation: ()-> Unit, + openConversation: () -> Unit, ) { val uiState = viewModel.uiState.collectAsStateWithLifecycle() @@ -82,8 +83,9 @@ internal fun ManualChargeDestination( onNavigateToSuccess(showCancellationWarning) }, onTriggerPayment = { - viewModel.emit(ManualChargeEvent.TriggerCharge) }, - openConversation = openConversation + viewModel.emit(ManualChargeEvent.TriggerCharge) + }, + openConversation = openConversation, ) } @@ -92,10 +94,10 @@ private fun ManualChargeScreen( uiState: ManualChargeUiState, navigateUp: () -> Unit, reload: () -> Unit, - openConversation: ()-> Unit, + openConversation: () -> Unit, onNavigateToPaymentDetails: (chargeId: String) -> Unit, onNavigateToSuccess: (Boolean) -> Unit, - onTriggerPayment: () -> Unit + onTriggerPayment: () -> Unit, ) { HedvigScaffold( navigateUp = navigateUp, @@ -104,17 +106,24 @@ private fun ManualChargeScreen( when (uiState) { is ManualChargeUiState.Failure -> { - val subTitle = if (uiState.error.message!=null) uiState.error.message else + val title = if (uiState.error.message != null) + stringResource(Res.string.SELF_MANUAL_CHARGE_CHANGES_BEEN_MADE_TITLE) else + stringResource(Res.string.something_went_wrong) + val subTitle = if (uiState.error.message != null) uiState.error.message else stringResource(Res.string.GENERAL_ERROR_BODY) - val buttonText = if (uiState.error.message!=null) stringResource(Res.string.claim_status_detail_chat_button_description) else + val buttonText = if (uiState.error.message != null) + stringResource(Res.string.claim_status_detail_chat_button_description) else stringResource(Res.string.GENERAL_RETRY) - val onButtonClick = if (uiState.error.message!=null) openConversation else reload + val onButtonClick = if (uiState.error.message != null) openConversation else reload HedvigErrorSection( onButtonClick = onButtonClick, - Modifier.weight(1f).fillMaxWidth(), + Modifier + .weight(1f) + .fillMaxWidth(), subTitle = subTitle, - buttonText = buttonText + buttonText = buttonText, + title = title, ) } @@ -126,7 +135,7 @@ private fun ManualChargeScreen( } is ManualChargeUiState.Success -> { - if (uiState.navigateToSuccess!=null) { + if (uiState.navigateToSuccess != null) { LaunchedEffect(uiState.navigateToSuccess) { onNavigateToSuccess(uiState.manualChargeInfo.showCancellationWarning) } @@ -134,7 +143,7 @@ private fun ManualChargeScreen( ManualChargeSuccessScreen( uiState, onNavigateToPaymentDetails = onNavigateToPaymentDetails, - onTriggerPayment = onTriggerPayment + onTriggerPayment = onTriggerPayment, ) } } @@ -280,7 +289,7 @@ private fun ManualChargeSuccessScreen( text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_PAY, uiState.manualChargeInfo.amountDue), onClick = onTriggerPayment, enabled = true, - buttonSize = ButtonDefaults.ButtonSize.Medium, + buttonSize = ButtonDefaults.ButtonSize.Medium, modifier = Modifier.fillMaxWidth(), ) @@ -299,7 +308,9 @@ private fun ManualChargeSuccessScreen( HedvigNotificationCard( message = stringResource(Res.string.MANUAL_CHARGE_CANCELLATION_WARNING), priority = NotificationDefaults.NotificationPriority.AttentionRound, - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), ) Spacer(Modifier.height(16.dp)) } @@ -324,7 +335,7 @@ private fun ManualChargeScreenSuccessPreview( chargeId = "chargeId", bankDescriptor = "**** 8324", bankAccountDisplayValue = "Swedbank", - showCancellationWarning = showCancellationWarning + showCancellationWarning = showCancellationWarning, ), navigateToSuccess = null, ), @@ -333,7 +344,7 @@ private fun ManualChargeScreenSuccessPreview( {}, {}, {}, - {} + {}, ) } } @@ -352,7 +363,7 @@ private fun ManualChargeScreenLoadingPreview() { {}, {}, {}, - {} + {}, ) } } @@ -369,15 +380,18 @@ private fun ManualChargeScreenFailurePreview( HedvigTheme { Surface { ManualChargeScreen( - uiState = ManualChargeUiState.Failure(ErrorMessage( - message = if (hasUserError) "Cannot charge the failed payment since there have been some changes. " + - "The new amount will be included in the upcoming payment." else null )), + uiState = ManualChargeUiState.Failure( + ErrorMessage( + message = if (hasUserError) "Cannot charge the failed payment since there have been some changes. " + + "The new amount will be included in the upcoming payment." else null, + ), + ), navigateUp = {}, reload = {}, {}, {}, {}, - {} + {}, ) } } diff --git a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/di/NotificationBadgeModule.kt b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/di/NotificationBadgeModule.kt index 3ff74b26cf..738bfdbd7b 100644 --- a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/di/NotificationBadgeModule.kt +++ b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/di/NotificationBadgeModule.kt @@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import com.apollographql.apollo.ApolloClient import com.hedvig.android.core.demomode.DemoManager +import com.hedvig.android.featureflags.FeatureManager import com.hedvig.android.notification.badge.data.crosssell.CrossSellNotificationBadgeService import com.hedvig.android.notification.badge.data.crosssell.GetCrossSellRecommendationIdUseCase import com.hedvig.android.notification.badge.data.crosssell.GetCrossSellRecommendationIdUseCaseImpl @@ -47,7 +48,7 @@ val notificationBadgeModule = module { ) } single { - GetIfMissedPaymentUseCaseImpl(get()) + GetIfMissedPaymentUseCaseImpl(get(), get()) } single { MissedPaymentNotificationServiceImpl(get()) From 0e5eb03ef1d732b9911f2b2cf28ca949fe647241 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 8 May 2026 16:39:21 +0200 Subject: [PATCH 25/29] break from loop if first false --- .../data/payment/GetIfMissedPaymentUseCase.kt | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetIfMissedPaymentUseCase.kt b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetIfMissedPaymentUseCase.kt index 686d730ef1..fb27d75a0e 100644 --- a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetIfMissedPaymentUseCase.kt +++ b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/payment/GetIfMissedPaymentUseCase.kt @@ -13,7 +13,6 @@ import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map @@ -38,26 +37,32 @@ internal class GetIfMissedPaymentUseCaseImpl( } while (currentCoroutineContext().isActive) { - emitAll( - apolloClient - .query(MissedPaymentQuery()) - .fetchPolicy(FetchPolicy.CacheAndNetwork) - .safeFlow { - logcat { "GetIfMissedPaymentUseCaseImpl error: $it" } - ErrorMessage() - } - .map { result -> - result.fold( - { - logcat { "GetIfMissedPaymentUseCaseImpl: error when loading missed payment: $it" } - false - }, - { data -> - data.currentMember.missedChargeIdToChargeManually != null - }, - ) - }, - ) + val hasMissedPayment = apolloClient + .query(MissedPaymentQuery()) + .fetchPolicy(FetchPolicy.CacheAndNetwork) + .safeFlow { + logcat { "GetIfMissedPaymentUseCaseImpl error: $it" } + ErrorMessage() + } + .map { result -> + result.fold( + { + logcat { "GetIfMissedPaymentUseCaseImpl: error when loading missed payment: $it" } + false + }, + { data -> + data.currentMember.missedChargeIdToChargeManually != null + }, + ) + } + .firstOrNull() ?: false + + emit(hasMissedPayment) + + if (!hasMissedPayment) { + break + } + delay(15.seconds) } } From a02131b68450f85e14c9a7a40198794eded33661 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 8 May 2026 16:42:08 +0200 Subject: [PATCH 26/29] remove todo --- .../hedvig/android/navigation/core/HedvigDeepLinkContainer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt b/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt index e2e0f55c49..d8aa749936 100644 --- a/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt +++ b/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt @@ -202,7 +202,7 @@ internal class HedvigDeepLinkContainerImpl( } override val manualCharge: List = baseDeepLinkDomains.map { baseDeepLinkDomain -> - "$baseDeepLinkDomain/manual-charge" //todo: check with other platforms + "$baseDeepLinkDomain/manual-charge" } } From ed810c3ad2930f620bda47cdbe4da7ce3f46f3a2 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 8 May 2026 16:46:08 +0200 Subject: [PATCH 27/29] logs --- .../feature/payments/data/GetManualChargeInfoUseCase.kt | 9 ++++----- .../feature/payments/data/TriggerManualChargeUseCase.kt | 6 +++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt index 77dfa2fef7..cf17b429ca 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt @@ -5,7 +5,7 @@ import arrow.core.raise.context.bind import arrow.core.raise.context.either import arrow.core.raise.context.raise import com.apollographql.apollo.ApolloClient -import com.apollographql.apollo.cache.normalized.FetchPolicy.NetworkFirst +import com.apollographql.apollo.cache.normalized.FetchPolicy import com.apollographql.apollo.cache.normalized.fetchPolicy import com.hedvig.android.apollo.ErrorMessage import com.hedvig.android.apollo.safeExecute @@ -14,7 +14,6 @@ import com.hedvig.android.core.uidata.UiMoney import com.hedvig.android.featureflags.FeatureManager import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.logger.logcat -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.datetime.LocalDate import octopus.ManualChargeInfoQuery @@ -37,7 +36,7 @@ internal class GetManualChargeInfoUseCaseImpl( } val currentMember = apolloClient.query(ManualChargeInfoQuery()) - .fetchPolicy(NetworkFirst) + .fetchPolicy(FetchPolicy.NetworkOnly) .safeExecute(::ErrorMessage) .bind() .currentMember @@ -67,7 +66,7 @@ internal class GetManualChargeInfoUseCaseImpl( amountDue = UiMoney.fromMoneyFragment(latestFailedPastCharge.net), bankAccountDisplayValue = currentMember.paymentInformation.chargeMethod?.displayName, bankDescriptor = currentMember.paymentInformation.chargeMethod?.descriptor, - showCancellationWarning = showCancellationWarning + showCancellationWarning = showCancellationWarning, ) } } @@ -78,5 +77,5 @@ internal data class ManualChargeInfo( val amountDue: UiMoney, val bankDescriptor: String?, val bankAccountDisplayValue: String?, - val showCancellationWarning: Boolean + val showCancellationWarning: Boolean, ) diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt index 0b4e4194ca..1f0ac79541 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt @@ -8,6 +8,7 @@ import com.apollographql.apollo.ApolloClient import com.hedvig.android.apollo.safeExecute import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.apollo.ErrorMessage +import com.hedvig.android.logger.logcat import kotlinx.datetime.LocalDate import octopus.ManuallyChargeMemberMutation @@ -22,7 +23,10 @@ internal class TriggerManualChargeUseCaseImpl( val result = apolloClient .mutation(ManuallyChargeMemberMutation()) .safeExecute() - .mapLeft { raise(ErrorMessage()) } + .mapLeft { + logcat { "TriggerManualChargeUseCase error: $it" } + raise(ErrorMessage()) + } .bind() if (result.manuallyChargeMember.userError!=null) raise(ErrorMessage( From b7812552d503f5efaa8794dd4da97272d93ad6e2 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 8 May 2026 16:49:18 +0200 Subject: [PATCH 28/29] rename MemberPaymentChargeMethod.Kivra to invoice --- .../hedvig/android/feature/payments/PreviewData.kt | 8 ++++---- .../android/feature/payments/data/MemberCharge.kt | 3 +-- .../feature/payments/data/PaymentOverview.kt | 2 +- .../ui/details/PaymentDetailsDestination.kt | 14 ++++++-------- 4 files changed, 12 insertions(+), 15 deletions(-) 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 37ad7806fe..9c5d7783df 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 @@ -156,7 +156,7 @@ internal val chargeHistoryPreviewData = listOf( carriedAdjustment = UiMoney(200.0, UiCurrencyCode.SEK), settlementAdjustment = UiMoney(200.0, UiCurrencyCode.SEK), referralDiscount = referralDiscountPreviewData, - chargeMethod = MemberPaymentChargeMethod.KIVRA, + chargeMethod = MemberPaymentChargeMethod.INVOICE, ), MemberCharge( grossAmount = UiMoney(200.0, UiCurrencyCode.SEK), @@ -258,7 +258,7 @@ internal val chargeHistoryPreviewData = listOf( carriedAdjustment = UiMoney(200.0, UiCurrencyCode.SEK), settlementAdjustment = UiMoney(200.0, UiCurrencyCode.SEK), referralDiscount = referralDiscountPreviewData, - chargeMethod = MemberPaymentChargeMethod.KIVRA, + chargeMethod = MemberPaymentChargeMethod.INVOICE, ), ) @@ -318,7 +318,7 @@ internal val paymentDetailsPreviewData = MemberCharge( chargeMethod = MemberPaymentChargeMethod.TRUSTLY, ) -internal val paymentDetailsKivraPreviewData = MemberCharge( +internal val paymentDetailsINVOICEPreviewData = MemberCharge( grossAmount = UiMoney(280.0, UiCurrencyCode.SEK), netAmount = UiMoney(200.0, UiCurrencyCode.SEK), id = "123", @@ -346,5 +346,5 @@ internal val paymentDetailsKivraPreviewData = MemberCharge( referralDiscount = referralDiscountPreviewData, carriedAdjustment = UiMoney(200.0, UiCurrencyCode.SEK), settlementAdjustment = UiMoney(200.0, UiCurrencyCode.SEK), - chargeMethod = MemberPaymentChargeMethod.KIVRA, + chargeMethod = MemberPaymentChargeMethod.INVOICE, ) diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt index 1411ea149e..4ed7d586cf 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt @@ -7,7 +7,6 @@ import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate import kotlinx.datetime.daysUntil import kotlinx.datetime.plus -import kotlinx.datetime.toJavaLocalDate import kotlinx.serialization.Serializable import octopus.PaymentHistoryWithDetailsQuery import octopus.ShortPaymentHistoryQuery @@ -170,7 +169,7 @@ internal fun MemberChargeFragment.toMemberCharge( internal fun String?.toChargeMethod(): MemberPaymentChargeMethod { return when { this?.startsWith("kivra", ignoreCase = true) == true || - this?.startsWith("invoice", ignoreCase = true) == true -> MemberPaymentChargeMethod.KIVRA + this?.startsWith("invoice", ignoreCase = true) == true -> MemberPaymentChargeMethod.INVOICE this?.startsWith("trustly", ignoreCase = true) == true -> MemberPaymentChargeMethod.TRUSTLY else -> MemberPaymentChargeMethod.UNKNOWN } 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 01804abc00..3f33016d69 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 @@ -30,6 +30,6 @@ internal data class MemberChargeShortInfo( enum class MemberPaymentChargeMethod { TRUSTLY, - KIVRA, + INVOICE, UNKNOWN, } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsDestination.kt index 6cc057ffd4..0af14825b6 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsDestination.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.hedvig.android.compose.ui.preview.BooleanCollectionPreviewParameterProvider import com.hedvig.android.compose.ui.preview.TripleBooleanCollectionPreviewParameterProvider import com.hedvig.android.compose.ui.preview.TripleCase import com.hedvig.android.design.system.hedvig.HedvigBottomSheet @@ -57,11 +56,10 @@ import com.hedvig.android.feature.payments.chargeHistoryPreviewData import com.hedvig.android.feature.payments.data.MemberCharge import com.hedvig.android.feature.payments.data.MemberPaymentChargeMethod import com.hedvig.android.feature.payments.data.PaymentDetails -import com.hedvig.android.feature.payments.paymentDetailsKivraPreviewData +import com.hedvig.android.feature.payments.paymentDetailsINVOICEPreviewData import com.hedvig.android.feature.payments.paymentDetailsPreviewData import com.hedvig.android.feature.payments.ui.discounts.DiscountRow import com.hedvig.android.feature.payments.ui.discounts.ForeverExplanationBottomSheet -import com.hedvig.android.logger.logcat import hedvig.resources.KIVRA_PAYMENT_INFO import hedvig.resources.PAYMENTS_ACCOUNT import hedvig.resources.PAYMENTS_AUTOGIRO_LABEL @@ -243,7 +241,7 @@ private fun MemberChargeDetailsScreen( MemberCharge.MemberChargeStatus.PENDING -> { val message = when (uiState.paymentDetails.memberCharge.chargeMethod) { MemberPaymentChargeMethod.TRUSTLY -> stringResource(Res.string.PAYMENTS_IN_PROGRESS) - MemberPaymentChargeMethod.KIVRA -> stringResource(Res.string.PAYMENTS_IN_PROGRESS_KIVRA) + MemberPaymentChargeMethod.INVOICE -> stringResource(Res.string.PAYMENTS_IN_PROGRESS_KIVRA) MemberPaymentChargeMethod.UNKNOWN -> null } if (message != null) { @@ -285,7 +283,7 @@ private fun MemberChargeDetailsScreen( endSlot = { when (uiState.paymentDetails.memberCharge.chargeMethod) { MemberPaymentChargeMethod.TRUSTLY, - MemberPaymentChargeMethod.KIVRA, + MemberPaymentChargeMethod.INVOICE, -> { val textToShow: String = if (uiState.paymentDetails.memberCharge.chargeMethod == MemberPaymentChargeMethod.TRUSTLY) { @@ -360,7 +358,7 @@ private fun MemberChargeDetailsScreen( when (val chargeMethod = uiState.paymentDetails.memberCharge.chargeMethod) { MemberPaymentChargeMethod.TRUSTLY, - MemberPaymentChargeMethod.KIVRA, + MemberPaymentChargeMethod.INVOICE, -> { HorizontalItemsWithMaximumSpaceTaken( startSlot = { @@ -369,7 +367,7 @@ private fun MemberChargeDetailsScreen( endSlot = { val text = when (chargeMethod) { MemberPaymentChargeMethod.TRUSTLY -> stringResource(Res.string.PAYMENTS_AUTOGIRO_LABEL) - MemberPaymentChargeMethod.KIVRA -> stringResource(Res.string.PAYMENTS_INVOICE) + MemberPaymentChargeMethod.INVOICE -> stringResource(Res.string.PAYMENTS_INVOICE) else -> "" } HedvigText( @@ -483,7 +481,7 @@ private fun PaymentDetailsScreenPreview( PaymentDetails( memberCharge = when (withPaymentInfo) { TripleCase.FIRST -> paymentDetailsPreviewData - TripleCase.SECOND -> paymentDetailsKivraPreviewData + TripleCase.SECOND -> paymentDetailsINVOICEPreviewData TripleCase.THIRD -> paymentDetailsPreviewData }, pastCharges = chargeHistoryPreviewData, From f75c1fd46954bdcadb18000597f9583e82404b8e Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Fri, 8 May 2026 17:01:13 +0200 Subject: [PATCH 29/29] add one more ff with include --- .../src/main/graphql/QueryUpcomingPayment.graphql | 6 ++++-- .../android/feature/payments/di/PaymentsModule.kt | 2 ++ .../overview/data/GetUpcomingPaymentUseCase.kt | 13 +++++++++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql b/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql index 81decbd041..0ce1caf37b 100644 --- a/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql +++ b/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql @@ -1,6 +1,8 @@ -query UpcomingPayment { +query UpcomingPayment( + $manualChargeEnabled: Boolean! +) { currentMember { - missedChargeIdToChargeManually + missedChargeIdToChargeManually @include(if: $manualChargeEnabled) pastCharges { ...on MemberCharge { id diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt index c85fc99ab5..95bb6d9e2c 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt @@ -58,6 +58,7 @@ val paymentsModule = module { GetUpcomingPaymentUseCaseImpl( apolloClient = get(), clock = get(), + featureManager = get() ) } single { @@ -120,6 +121,7 @@ val paymentsModule = module { GetUpcomingPaymentUseCaseImpl( get(), clock = get(), + featureManager = get() ) } single { 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 7e660417d7..f98d235568 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 @@ -18,8 +18,11 @@ 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.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.Feature import kotlin.time.Clock import kotlin.time.Duration.Companion.days +import kotlinx.coroutines.flow.firstOrNull import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import octopus.UpcomingPaymentQuery @@ -34,14 +37,20 @@ internal interface GetUpcomingPaymentUseCase { internal data class GetUpcomingPaymentUseCaseImpl( val apolloClient: ApolloClient, val clock: Clock, + val featureManager: FeatureManager ) : GetUpcomingPaymentUseCase { override suspend fun invoke(): Either = either { - val result = apolloClient.query(UpcomingPaymentQuery()) + + val isFeatureFlagOn = featureManager.isFeatureEnabled(Feature.ENABLE_MANUAL_CHARGE).firstOrNull() ?: false + + val result = apolloClient.query(UpcomingPaymentQuery(isFeatureFlagOn)) .fetchPolicy(FetchPolicy.NetworkFirst) .safeExecute(::ErrorMessage) .bind() - val missedChargeIdToChargeManually: String? = result.currentMember.missedChargeIdToChargeManually + + val missedChargeIdToChargeManually: String? = if (isFeatureFlagOn) + result.currentMember.missedChargeIdToChargeManually else null val isManualChargeAllowed = if (missedChargeIdToChargeManually != null) { val failedChargeNet = result.currentMember.pastCharges.firstOrNull {