From 97ee45839dc8b2c25b22398c6b56c5566d6b9260 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Thu, 16 Apr 2026 17:10:02 +0200 Subject: [PATCH 01/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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) },