diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index d0778a65ac..41bbee29b1 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -3,6 +3,7 @@ on: push: branches: - develop + - feat/manual-charge workflow_dispatch: concurrency: 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 3dfae62c09..a3e09a71dd 100644 --- a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls +++ b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls @@ -2527,6 +2527,11 @@ type Member { """ crossSellV2(input: CrossSellInput!): CrossSellV2! """ + Young Pet Guide stories for the member. + Returns a list of educational content stories for young pet owners. + """ + puppyGuideStories: [PuppyGuideStory!]! + """ Fetch all the active contracts for this member. Active contracts include all insurances that are either active today, or to-be-active in the future. """ @@ -3650,8 +3655,10 @@ type Mutation { """ Confirm this PriceIntent, which will use the current data (and likely `ShopSession.customer`) to generate `ProductOffers` that can be added to the cart. + Optional `attribution` is merged field-by-field over `ShopSession.attribution` for this confirm only (quotes and + `PriceIntentConfirmed` events use the merged result). """ - priceIntentConfirm(priceIntentId: UUID!): PriceIntentMutationOutput! + priceIntentConfirm(priceIntentId: UUID!, attribution: PriceIntentConfirmAttributionInput): PriceIntentMutationOutput! """ Change the start date of the given `ProductOffer`s by their ID. This is used because it's common to want to change the start date AFTER getting the offer. @@ -3673,6 +3680,10 @@ type Mutation { """ productOfferReprice(offerId: UUID!, data: PricingFormData!): ProductOffersMutationOutput! """ + Mark a young pet guide story as read for a specific member. + """ + puppyGuideEngagement(engagement: PuppyEngagementInput!): PuppyGuideStoryMutationOutput! + """ Update the customer of the shop session. Only non-null fields will be changed. Can trigger automatic lookup of other information. The session can be placed in a "point of no return" state where it is no longer legal to update the customer, @@ -4015,6 +4026,36 @@ type PriceIntentAnimalBreed { displayName: String! isMixedBreed: Boolean! } +""" +Attribution values to apply when confirming a PriceIntent. +Provided fields override the ShopSession attribution for this confirmation only. +""" +input PriceIntentConfirmAttributionInput { + """ + Party credited for originating the flow, such as a partner, Hedvig, or marketing source. + """ + attributedTo: String + """ + Channel or surface where the flow was initiated. + """ + initiatedFrom: String + """ + High-level business flow used for attribution and analytics. + """ + userFlow: String + """ + More specific source within the user flow, when applicable. + """ + flowSource: String + """ + Experiment variants active when the price intent was confirmed. + """ + experiments: [ShopSessionExperimentInput!] + """ + Additional attribution metadata passed through for analytics integrations. + """ + trackingData: JSON +} input PriceIntentCreateInput { shopSessionId: UUID! """ @@ -4404,6 +4445,53 @@ type ProductVariantComparisonRow { """ covered: [String!]! } +input PuppyEngagementInput { + name: String! + rating: Int + opened: Boolean + read: Boolean + closed: Boolean +} +type PuppyGuideStory { + """ + The unique name/identifier of the story. + """ + name: String! + """ + The display title of the story. + """ + title: String! + """ + The subtitle or description of the story. + """ + subtitle: String! + """ + The main content of the story. + """ + content: String! + """ + The image associated with this story. + """ + image: String! + """ + Categories this story belongs to. + """ + categories: [String!]! + """ + The date when the story was marked as read by the user. + """ + read: Boolean! + """ + The user's rating of the story. + """ + rating: Int +} +type PuppyGuideStoryMutationOutput { + """ + Indicates whether the mutation was successful. + """ + success: Boolean! +} type Query { """ Return a conversation for a given ID. diff --git a/app/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/navigation/HedvigNavHost.kt b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigNavHost.kt index fa12d4c943..aff77558a5 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigNavHost.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigNavHost.kt @@ -346,6 +346,9 @@ internal fun HedvigNavHost( navigateToPayoutAccount = { navController.navigate(PayoutAccountDestination.Graph) }, languageService = languageService, hedvigBuildConstants = hedvigBuildConstants, + openConversation = { + navigateToNewConversation() + } ) payoutAccountGraph( navController = navController, diff --git a/app/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/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml index d0f3517425..1121f472a0 100644 --- a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml @@ -522,6 +522,7 @@ Fråga angående skadeanmälan - Fordon reg. %1$s Välj land och språk Logga ut + Aktivera ditt försäkringsskydd igen genom att kontakta oss när din betalning har registrerats Få ett prisförslag Se över kontaktuppgifter Se till att vi har rätt kontaktuppgifter ifall vi behöver nå dig. @@ -609,7 +610,7 @@ Denna betalning misslyckades och lades till i din betalning den %1$s . Betalningshistorik Betalning pågår - Det kan ta upp till 5 bankdagar innan betalningen syns + Det kan ta upp till 5 bankdagar innan betalningen registreras Betalningssätt Att betala: %1$s Vi kunde inte dra betalningen från ditt bankkonto. Betala för att undvika avbrott i ditt skydd. @@ -620,6 +621,8 @@ Betala %1$s Försenad sedan %1$s Visa betalningsdetaljer + Inga försenade betalningar + Allt är betalt Försenad betalning Betalning genomförd %1$s dagar @@ -709,6 +712,7 @@ Sök Välj den försäkring du vill uppdatera Select insurance + Betalningen kan inte genomföras Språk Logga in Notiser @@ -1025,9 +1029,9 @@ Din skadeanmälan granskas av en av våra försäkringsspecialister. Vi hör av oss snart med en uppdatering. Vi har återöppnat din skadeanmälan och en av våra försäkringsspecialister granskar den. Vi hör av oss snart med en uppdatering. Se detaljer - Reg nr + Registreringsnummer Ärendenummer - E-mail + Mail för ärende Saknas något? Skicka ett meddelande till oss här i appen. Inskickad Detaljer om skadeanmälan @@ -1037,7 +1041,7 @@ Din skada täcks, men tyvärr är ersättningen lika stor eller mindre än självrisken. Se konversation för mer information. Tyvärr täcks inte din skada. Se konversation för mer information om beslutet. Din skada har täckts. Du borde ha fått ersättningen utbetald vid det här laget. - Den här skadan hanteras av vår externa partner Eir via mail.\nRedan i kontakt med dem? Svara i din befintliga mailtråd så att all skadeinformation hålls samlad i en och samma konversation. + Våra bilspecialister på Eir hanterar din skadeanmälan via mail.\n\nNär de har gått igenom ärendet får du en bekräftelse via mail. Behöver du komma i kontakt kring din anmälan, svara i samma e-posttråd. Vi har tagit emot din skadeanmälan och kommer snart att börja granska den. Ladda upp foton, kvitton eller andra dokument kopplade till din skadeanmälan Besvärshänvisning diff --git a/app/core/core-resources/src/androidMain/res/values/strings.xml b/app/core/core-resources/src/androidMain/res/values/strings.xml index 853c6f6747..ab249a9e6d 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -522,6 +522,7 @@ Question regarding claim, Vehicle reg. %1$s Preferences Logout + Activate your coverage again by contacting us once your payment has been processed Get a price quote Review contact info Make sure we have the right contact info in case we need to reach you. @@ -609,7 +610,7 @@ This payment failed and was added to your payment on %1$s. Payment history Payment in progress - It may take up to 5 business days for the charge to appear + It may take up to 5 business days for the charge to process Payment method Amount due: %1$s We couldn\'t collect this payment from your bank account. Pay now to keep your coverage active. @@ -620,6 +621,8 @@ Pay %1$s Overdue since %1$s View payment details + No overdue payments + Payments up to date Payment overdue Payment successful %1$s days @@ -709,6 +712,7 @@ Search Select the insurance you want to edit Select insurance + Payment can\'t be charged Language Login Notifications @@ -1025,9 +1029,9 @@ Your claim is being reviewed by one of our insurance specialists. We\'ll get back to you soon with an update. We have reopened your claim and one of our insurance specialists is reviewing it. We\'ll get back to you soon with an update. Show details - Reg. no. + Registration number Reference number - Email + Claim email Is something missing? Send us a message here in the app. Submitted Claim details @@ -1037,7 +1041,7 @@ Your claim was covered but unfortunately the compensation is equal or less than the deductible. Please see conversation for more details. Your claim was unfortunately not covered. Please see conversation for more details on the decision. We got you covered. You should have received the payment by now. - This claim is handled by our external partner Eir by email.\nAlready in contact with them? Reply to your existing email thread to keep all claim details in one conversation. + Our car specialists at Eir handle this claim by email.\n\nYou’ll receive a confirmation by email once it’s been reviewed. If you need to get in touch about your claim, reply in the same email thread. We have received your claim and will start reviewing it soon. Upload photos, receipts or other documents connected to your claim Appeal Instruction (SWE) diff --git a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml index b2bd53ce12..ee88808b69 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -522,6 +522,7 @@ Fråga angående skadeanmälan - Fordon reg. %1$s Välj land och språk Logga ut + Aktivera ditt försäkringsskydd igen genom att kontakta oss när din betalning har registrerats Få ett prisförslag Se över kontaktuppgifter Se till att vi har rätt kontaktuppgifter ifall vi behöver nå dig. @@ -609,7 +610,7 @@ Denna betalning misslyckades och lades till i din betalning den %1$s . Betalningshistorik Betalning pågår - Det kan ta upp till 5 bankdagar innan betalningen syns + Det kan ta upp till 5 bankdagar innan betalningen registreras Betalningssätt Att betala: %1$s Vi kunde inte dra betalningen från ditt bankkonto. Betala för att undvika avbrott i ditt skydd. @@ -620,6 +621,8 @@ Betala %1$s Försenad sedan %1$s Visa betalningsdetaljer + Inga försenade betalningar + Allt är betalt Försenad betalning Betalning genomförd %1$s dagar @@ -709,6 +712,7 @@ Sök Välj den försäkring du vill uppdatera Select insurance + Betalningen kan inte genomföras Språk Logga in Notiser @@ -1025,9 +1029,9 @@ Din skadeanmälan granskas av en av våra försäkringsspecialister. Vi hör av oss snart med en uppdatering. Vi har återöppnat din skadeanmälan och en av våra försäkringsspecialister granskar den. Vi hör av oss snart med en uppdatering. Se detaljer - Reg nr + Registreringsnummer Ärendenummer - E-mail + Mail för ärende Saknas något? Skicka ett meddelande till oss här i appen. Inskickad Detaljer om skadeanmälan @@ -1037,7 +1041,7 @@ Din skada täcks, men tyvärr är ersättningen lika stor eller mindre än självrisken. Se konversation för mer information. Tyvärr täcks inte din skada. Se konversation för mer information om beslutet. Din skada har täckts. Du borde ha fått ersättningen utbetald vid det här laget. - Den här skadan hanteras av vår externa partner Eir via mail.\nRedan i kontakt med dem? Svara i din befintliga mailtråd så att all skadeinformation hålls samlad i en och samma konversation. + Våra bilspecialister på Eir hanterar din skadeanmälan via mail.\n\nNär de har gått igenom ärendet får du en bekräftelse via mail. Behöver du komma i kontakt kring din anmälan, svara i samma e-posttråd. Vi har tagit emot din skadeanmälan och kommer snart att börja granska den. Ladda upp foton, kvitton eller andra dokument kopplade till din skadeanmälan Besvärshänvisning diff --git a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml index cb2684c667..ff57e81a6a 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -522,6 +522,7 @@ Question regarding claim, Vehicle reg. %1$s Preferences Logout + Activate your coverage again by contacting us once your payment has been processed Get a price quote Review contact info Make sure we have the right contact info in case we need to reach you. @@ -609,7 +610,7 @@ This payment failed and was added to your payment on %1$s. Payment history Payment in progress - It may take up to 5 business days for the charge to appear + It may take up to 5 business days for the charge to process Payment method Amount due: %1$s We couldn't collect this payment from your bank account. Pay now to keep your coverage active. @@ -620,6 +621,8 @@ Pay %1$s Overdue since %1$s View payment details + No overdue payments + Payments up to date Payment overdue Payment successful %1$s days @@ -709,6 +712,7 @@ Search Select the insurance you want to edit Select insurance + Payment can't be charged Language Login Notifications @@ -1025,9 +1029,9 @@ Your claim is being reviewed by one of our insurance specialists. We'll get back to you soon with an update. We have reopened your claim and one of our insurance specialists is reviewing it. We'll get back to you soon with an update. Show details - Reg. no. + Registration number Reference number - Email + Claim email Is something missing? Send us a message here in the app. Submitted Claim details @@ -1037,7 +1041,7 @@ Your claim was covered but unfortunately the compensation is equal or less than the deductible. Please see conversation for more details. Your claim was unfortunately not covered. Please see conversation for more details on the decision. We got you covered. You should have received the payment by now. - This claim is handled by our external partner Eir by email.\nAlready in contact with them? Reply to your existing email thread to keep all claim details in one conversation. + Our car specialists at Eir handle this claim by email.\n\nYou’ll receive a confirmation by email once it’s been reviewed. If you need to get in touch about your claim, reply in the same email thread. We have received your claim and will start reviewing it soon. Upload photos, receipts or other documents connected to your claim Appeal Instruction (SWE) diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/EmptyState.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/EmptyState.kt index 3d96a407e7..567de7b4fd 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/EmptyState.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/EmptyState.kt @@ -138,6 +138,16 @@ private fun ColumnScope.EmptyStateIcon(iconStyle: EmptyStateIconStyle) { } NO_ICON -> {} + + EmptyStateIconStyle.SUCCESS_WITH_WARNING -> { + Icon( + HedvigIcons.CheckFilled, + null, + tint = emptyStateColors.errorIconColor, + modifier = sizeModifier, + ) + Spacer(Modifier.height(16.dp)) + } } } @@ -151,6 +161,7 @@ object EmptyStateDefaults { SUCCESS, BANK_ID, NO_ICON, + SUCCESS_WITH_WARNING } sealed class EmptyStateButtonStyle { diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/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/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt index 3366acd6cb..c4c4c03508 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt @@ -109,7 +109,7 @@ fun HedvigNotificationCard( ) { val padding = if (withIcon) paddingWithIcon else paddingNoIcon val description = when (priority) { - Attention, Error, Info -> stringResource(Res.string.TALKBACK_NOTIFICATION_CARD) + Attention, NotificationPriority.AttentionRound, Error, Info -> stringResource(Res.string.TALKBACK_NOTIFICATION_CARD) Campaign, InfoInline, NeutralToast, FancyInfo -> "" } @@ -292,6 +292,28 @@ object NotificationDefaults { get() = SecondaryAlt } + data object AttentionRound : NotificationPriority { + override val colors: NotificationColors + @Composable + get() = with(HedvigTheme.colorScheme) { + remember(this) { + NotificationColors( + containerColor = fromToken(SignalAmberFill), + borderColor = fromToken(SignalAmberFill), + textColor = fromToken(SignalAmberText), + iconColor = fromToken(SignalAmberElement), + ) + } + } + override val icon: ImageVector + @Composable + get() = + HedvigIcons.InfoFilled + + override val buttonStyle: ButtonDefaults.ButtonStyle + get() = SecondaryAlt + } + data object Error : NotificationPriority { override val colors: NotificationColors @Composable diff --git a/app/feature/feature-payments/build.gradle.kts b/app/feature/feature-payments/build.gradle.kts index 0a249e7b59..427d2fca10 100644 --- a/app/feature/feature-payments/build.gradle.kts +++ b/app/feature/feature-payments/build.gradle.kts @@ -10,33 +10,34 @@ hedvig { } dependencies { - implementation(libs.apollo.normalizedCache) - implementation(libs.apollo.runtime) - implementation(libs.arrow.core) - implementation(libs.arrow.fx) - implementation(libs.jetbrains.compose.foundation) - implementation(libs.jetbrains.compose.runtime) - implementation(libs.jetbrains.lifecycle.runtime.compose) - implementation(libs.jetbrains.navigation.compose) - implementation(libs.koin.composeViewModel) - implementation(libs.koin.core) - implementation(libs.kotlinx.serialization.core) - implementation(projects.apolloCore) - implementation(projects.apolloOctopusPublic) - implementation(projects.composeUi) - implementation(projects.coreBuildConstants) - implementation(projects.coreCommonPublic) - implementation(projects.coreDemoMode) - implementation(projects.coreResources) - implementation(projects.coreUiData) - implementation(projects.dataPayingMember) - implementation(projects.designSystemHedvig) - implementation(projects.foreverUi) - implementation(projects.languageCore) - implementation(projects.moleculePublic) - implementation(projects.navigationCommon) - implementation(projects.navigationCompose) - implementation(projects.navigationCore) - implementation(projects.pullrefresh) - implementation(projects.theme) + implementation(libs.apollo.normalizedCache) + implementation(libs.apollo.runtime) + implementation(libs.arrow.core) + implementation(libs.arrow.fx) + implementation(libs.jetbrains.compose.foundation) + implementation(libs.jetbrains.compose.runtime) + implementation(libs.jetbrains.lifecycle.runtime.compose) + implementation(libs.jetbrains.navigation.compose) + implementation(libs.koin.composeViewModel) + implementation(libs.koin.core) + implementation(libs.kotlinx.serialization.core) + implementation(projects.apolloCore) + implementation(projects.apolloOctopusPublic) + implementation(projects.composeUi) + implementation(projects.coreBuildConstants) + implementation(projects.coreCommonPublic) + implementation(projects.coreDemoMode) + implementation(projects.coreResources) + implementation(projects.coreUiData) + implementation(projects.dataPayingMember) + implementation(projects.designSystemHedvig) + implementation(projects.featureFlagsPublic) + implementation(projects.foreverUi) + implementation(projects.languageCore) + implementation(projects.moleculePublic) + implementation(projects.navigationCommon) + implementation(projects.navigationCompose) + implementation(projects.navigationCore) + implementation(projects.pullrefresh) + implementation(projects.theme) } diff --git a/app/feature/feature-payments/src/main/graphql/MutationManuallyChargeMember.graphql b/app/feature/feature-payments/src/main/graphql/MutationManuallyChargeMember.graphql new file mode 100644 index 0000000000..8767d2322a --- /dev/null +++ b/app/feature/feature-payments/src/main/graphql/MutationManuallyChargeMember.graphql @@ -0,0 +1,7 @@ +mutation ManuallyChargeMember { + manuallyChargeMember { + userError { + message + } + } +} 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..da34fe73c5 --- /dev/null +++ b/app/feature/feature-payments/src/main/graphql/QueryManualChargeInfo.graphql @@ -0,0 +1,27 @@ +query ManualChargeInfo { + currentMember { + missedChargeIdToChargeManually + pastCharges { + ...on MemberCharge { + id + date + status + net { + ...MoneyFragment + } + } + } + paymentInformation { + status + chargeMethod { + displayName + descriptor + paymentMethod + } + } + activeContracts { + terminationDueToMissedPayments + terminationDate + } + } +} diff --git a/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql b/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql index 97301b6afb..0ce1caf37b 100644 --- a/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql +++ b/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql @@ -1,5 +1,17 @@ -query UpcomingPayment { +query UpcomingPayment( + $manualChargeEnabled: Boolean! +) { currentMember { + missedChargeIdToChargeManually @include(if: $manualChargeEnabled) + 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/PreviewData.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/PreviewData.kt index b93a6f2cd9..9c5d7783df 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/PreviewData.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/PreviewData.kt @@ -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( @@ -153,7 +156,7 @@ internal val chargeHistoryPreviewData = listOf( carriedAdjustment = UiMoney(200.0, UiCurrencyCode.SEK), settlementAdjustment = UiMoney(200.0, UiCurrencyCode.SEK), referralDiscount = referralDiscountPreviewData, - chargeMethod = MemberPaymentChargeMethod.KIVRA, + chargeMethod = MemberPaymentChargeMethod.INVOICE, ), MemberCharge( grossAmount = UiMoney(200.0, UiCurrencyCode.SEK), @@ -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( @@ -252,7 +258,7 @@ internal val chargeHistoryPreviewData = listOf( carriedAdjustment = UiMoney(200.0, UiCurrencyCode.SEK), settlementAdjustment = UiMoney(200.0, UiCurrencyCode.SEK), referralDiscount = referralDiscountPreviewData, - chargeMethod = MemberPaymentChargeMethod.KIVRA, + chargeMethod = MemberPaymentChargeMethod.INVOICE, ), ) @@ -266,12 +272,14 @@ internal val paymentOverViewPreviewData: PaymentOverview failedCharge = MemberCharge.FailedCharge( fromDate = LocalDate.fromEpochDays(200), toDate = LocalDate.fromEpochDays(201), + sum = UiMoney(200.0, UiCurrencyCode.SEK), ), ) return PaymentOverview( memberChargeShortInfo = memberChargeShortInfo, ongoingCharges = listOf(OngoingCharge("id", LocalDate.fromEpochDays(401), UiMoney(200.0, UiCurrencyCode.SEK))), paymentConnection = PaymentConnection.Active, + isManualChargeAllowed = ManualChargeToPrompt(UiMoney(200.0, UiCurrencyCode.SEK)), ) } @@ -284,6 +292,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( @@ -309,7 +318,7 @@ internal val paymentDetailsPreviewData = MemberCharge( chargeMethod = MemberPaymentChargeMethod.TRUSTLY, ) -internal val paymentDetailsKivraPreviewData = MemberCharge( +internal val paymentDetailsINVOICEPreviewData = MemberCharge( grossAmount = UiMoney(280.0, UiCurrencyCode.SEK), netAmount = UiMoney(200.0, UiCurrencyCode.SEK), id = "123", @@ -337,5 +346,5 @@ internal val paymentDetailsKivraPreviewData = MemberCharge( referralDiscount = referralDiscountPreviewData, carriedAdjustment = UiMoney(200.0, UiCurrencyCode.SEK), settlementAdjustment = UiMoney(200.0, UiCurrencyCode.SEK), - chargeMethod = MemberPaymentChargeMethod.KIVRA, + chargeMethod = MemberPaymentChargeMethod.INVOICE, ) diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt new file mode 100644 index 0000000000..cf17b429ca --- /dev/null +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt @@ -0,0 +1,81 @@ +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 +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 com.hedvig.android.logger.logcat +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.datetime.LocalDate +import octopus.ManualChargeInfoQuery + +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 isFeatureFlagOn = featureManager.isFeatureEnabled(Feature.ENABLE_MANUAL_CHARGE).firstOrNull() ?: false + + if (!isFeatureFlagOn) { + logcat { "ENABLE_MANUAL_CHARGE flag is off" } + raise(ErrorMessage()) + } + + val currentMember = apolloClient.query(ManualChargeInfoQuery()) + .fetchPolicy(FetchPolicy.NetworkOnly) + .safeExecute(::ErrorMessage) + .bind() + .currentMember + + val showManualCharge = currentMember.missedChargeIdToChargeManually + + val showCancellationWarning = + currentMember.activeContracts + .any { it.terminationDueToMissedPayments && it.terminationDate != null } + + if (showManualCharge == null) { + logcat { "GetManualChargeInfoUseCaseImpl: missedChargeIdToChargeManually is null" } + raise(ErrorMessage()) + } + + val latestFailedPastCharge = currentMember.pastCharges + .firstOrNull { it.id == showManualCharge } + + 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, + showCancellationWarning = showCancellationWarning, + ) + } +} + +internal data class ManualChargeInfo( + val chargeId: String?, + val missedDueDate: LocalDate, + val amountDue: UiMoney, + val bankDescriptor: String?, + val bankAccountDisplayValue: String?, + val showCancellationWarning: Boolean, +) diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt index 9f98e86fc5..4ed7d586cf 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt @@ -44,6 +44,7 @@ internal data class MemberCharge( data class FailedCharge( val fromDate: LocalDate, val toDate: LocalDate, + val sum: UiMoney, ) internal enum class MemberChargeStatus { @@ -167,7 +168,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.INVOICE this?.startsWith("trustly", ignoreCase = true) == true -> MemberPaymentChargeMethod.TRUSTLY else -> MemberPaymentChargeMethod.UNKNOWN } @@ -180,11 +182,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 { + 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..3f33016d69 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentOverview.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentOverview.kt @@ -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, @@ -25,6 +30,6 @@ internal data class MemberChargeShortInfo( enum class MemberPaymentChargeMethod { TRUSTLY, - KIVRA, + INVOICE, UNKNOWN, } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/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..1f0ac79541 --- /dev/null +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt @@ -0,0 +1,36 @@ +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 com.hedvig.android.logger.logcat +import kotlinx.datetime.LocalDate +import octopus.ManuallyChargeMemberMutation + +internal interface TriggerManualChargeUseCase { + suspend fun invoke(): Either +} + +internal class TriggerManualChargeUseCaseImpl( + private val apolloClient: ApolloClient +): TriggerManualChargeUseCase { + override suspend fun invoke(): Either = either { + val result = apolloClient + .mutation(ManuallyChargeMemberMutation()) + .safeExecute() + .mapLeft { + logcat { "TriggerManualChargeUseCase error: $it" } + raise(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 4471be10e5..95bb6d9e2c 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt @@ -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.GetShouldShowPayoutUseCase @@ -26,8 +30,10 @@ 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 import kotlin.time.Clock import org.koin.core.module.dsl.viewModel import org.koin.dsl.module @@ -50,8 +56,9 @@ val paymentsModule = module { } single { GetUpcomingPaymentUseCaseImpl( - get(), - get(), + apolloClient = get(), + clock = get(), + featureManager = get() ) } single { @@ -114,6 +121,7 @@ val paymentsModule = module { GetUpcomingPaymentUseCaseImpl( get(), clock = get(), + featureManager = get() ) } single { @@ -121,6 +129,24 @@ val paymentsModule = module { clock = get(), ) } + + single { + TriggerManualChargeUseCaseImpl(get(),) + } + + single { + GetManualChargeInfoUseCaseImpl( + get(), + get() + ) + } + + viewModel { + ManualChargeViewModel( + get(), + get(), + ) + } single { GetShouldShowPayoutUseCaseProvider( demoManager = get(), 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..e6a9028f52 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt @@ -28,4 +28,13 @@ internal sealed interface PaymentsDestinations { @Serializable data object MemberPaymentDetails : PaymentsDestinations, Destination + + @Serializable + data object ManualCharge: PaymentsDestinations, Destination + + @Serializable + data class ManualChargeSuccess( + val showCancellationWarning: Boolean + ): PaymentsDestinations, Destination } + diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt index 985acc1e86..08804dd98b 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt @@ -12,6 +12,9 @@ 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.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 import com.hedvig.android.feature.payments.ui.payments.PaymentsDestination @@ -20,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 @@ -33,6 +37,7 @@ fun NavGraphBuilder.paymentsGraph( hedvigBuildConstants: HedvigBuildConstants, navigateToConnectPayment: () -> Unit, navigateToPayoutAccount: () -> Unit, + openConversation: () -> Unit, ) { navgraph( startDestination = PaymentsDestination.Payments::class, @@ -59,9 +64,45 @@ fun NavGraphBuilder.paymentsGraph( onMemberPaymentDetailsClicked = dropUnlessResumed { navController.navigate(PaymentsDestinations.MemberPaymentDetails) }, + onOpenManualCharge = { + navController.navigate(PaymentsDestinations.ManualCharge) + }, ) } + navdestination( + deepLinks = navDeepLinks(hedvigDeepLinkContainer.manualCharge), + ) { + val viewModel: ManualChargeViewModel = koinViewModel() + ManualChargeDestination( + viewModel = viewModel, + navigateUp = navController::navigateUp, + onNavigateToPaymentDetails = dropUnlessResumed { chargeId: String -> + navController.navigate( + PaymentsDestinations.Details( + chargeId, + ), + ) + }, + onNavigateToSuccess = { showCancellationWarning -> + navController.navigate(PaymentsDestinations.ManualChargeSuccess( + showCancellationWarning = showCancellationWarning + )) { + typedPopUpTo { + inclusive = true + } + } + }, + openConversation = openConversation + ) + } + + navdestination{ + ManualChargeSuccessDestination( + this.showCancellationWarning, + 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 823118594c..f98d235568 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt @@ -11,14 +11,18 @@ import com.hedvig.android.apollo.safeExecute import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.feature.payments.data.ManualChargeToPrompt import com.hedvig.android.feature.payments.data.MemberCharge import com.hedvig.android.feature.payments.data.MemberChargeShortInfo import com.hedvig.android.feature.payments.data.PaymentConnection import com.hedvig.android.feature.payments.data.PaymentOverview import com.hedvig.android.feature.payments.data.PaymentOverview.OngoingCharge import com.hedvig.android.feature.payments.data.toFailedCharge +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.Feature import kotlin.time.Clock import kotlin.time.Duration.Companion.days +import kotlinx.coroutines.flow.firstOrNull import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import octopus.UpcomingPaymentQuery @@ -33,13 +37,34 @@ internal interface GetUpcomingPaymentUseCase { internal data class GetUpcomingPaymentUseCaseImpl( val apolloClient: ApolloClient, val clock: Clock, + val featureManager: FeatureManager ) : GetUpcomingPaymentUseCase { override suspend fun invoke(): Either = either { - val result = apolloClient.query(UpcomingPaymentQuery()) + + val isFeatureFlagOn = featureManager.isFeatureEnabled(Feature.ENABLE_MANUAL_CHARGE).firstOrNull() ?: false + + val result = apolloClient.query(UpcomingPaymentQuery(isFeatureFlagOn)) .fetchPolicy(FetchPolicy.NetworkFirst) .safeExecute(::ErrorMessage) .bind() + + val missedChargeIdToChargeManually: String? = if (isFeatureFlagOn) + result.currentMember.missedChargeIdToChargeManually else null + + val isManualChargeAllowed = if (missedChargeIdToChargeManually != null) { + val failedChargeNet = result.currentMember.pastCharges.firstOrNull { + it.id == missedChargeIdToChargeManually + }?.net?.let { net -> + UiMoney.fromMoneyFragment(net) + } + if (failedChargeNet != null) { + ManualChargeToPrompt(failedChargeNet) + } else null + } else { + null + } + PaymentOverview( memberChargeShortInfo = result.currentMember.futureCharge?.toMemberChargeShortInfo(), ongoingCharges = result.currentMember.ongoingCharges.mapNotNull { @@ -58,7 +83,9 @@ internal data class GetUpcomingPaymentUseCaseImpl( .mapNotNull { it.terminationDate } .sorted() .firstOrNull() - return@run PaymentConnection.NeedsSetup(firstKnownTerminationDateForContractTerminatedDueToMissedPayments) + return@run PaymentConnection.NeedsSetup( + firstKnownTerminationDateForContractTerminatedDueToMissedPayments, + ) } when (payinMethod.status) { MemberPaymentMethodStatus.ACTIVE -> PaymentConnection.Active @@ -66,6 +93,7 @@ internal data class GetUpcomingPaymentUseCaseImpl( MemberPaymentMethodStatus.UNKNOWN__ -> PaymentConnection.Unknown } }, + isManualChargeAllowed = isManualChargeAllowed, ) } } @@ -98,6 +126,7 @@ internal class GetUpcomingPaymentUseCaseDemo( ), emptyList(), PaymentConnection.Unknown, + isManualChargeAllowed = null, ).right() } } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsDestination.kt index 6cc057ffd4..0af14825b6 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsDestination.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.hedvig.android.compose.ui.preview.BooleanCollectionPreviewParameterProvider import com.hedvig.android.compose.ui.preview.TripleBooleanCollectionPreviewParameterProvider import com.hedvig.android.compose.ui.preview.TripleCase import com.hedvig.android.design.system.hedvig.HedvigBottomSheet @@ -57,11 +56,10 @@ import com.hedvig.android.feature.payments.chargeHistoryPreviewData import com.hedvig.android.feature.payments.data.MemberCharge import com.hedvig.android.feature.payments.data.MemberPaymentChargeMethod import com.hedvig.android.feature.payments.data.PaymentDetails -import com.hedvig.android.feature.payments.paymentDetailsKivraPreviewData +import com.hedvig.android.feature.payments.paymentDetailsINVOICEPreviewData import com.hedvig.android.feature.payments.paymentDetailsPreviewData import com.hedvig.android.feature.payments.ui.discounts.DiscountRow import com.hedvig.android.feature.payments.ui.discounts.ForeverExplanationBottomSheet -import com.hedvig.android.logger.logcat import hedvig.resources.KIVRA_PAYMENT_INFO import hedvig.resources.PAYMENTS_ACCOUNT import hedvig.resources.PAYMENTS_AUTOGIRO_LABEL @@ -243,7 +241,7 @@ private fun MemberChargeDetailsScreen( MemberCharge.MemberChargeStatus.PENDING -> { val message = when (uiState.paymentDetails.memberCharge.chargeMethod) { MemberPaymentChargeMethod.TRUSTLY -> stringResource(Res.string.PAYMENTS_IN_PROGRESS) - MemberPaymentChargeMethod.KIVRA -> stringResource(Res.string.PAYMENTS_IN_PROGRESS_KIVRA) + MemberPaymentChargeMethod.INVOICE -> stringResource(Res.string.PAYMENTS_IN_PROGRESS_KIVRA) MemberPaymentChargeMethod.UNKNOWN -> null } if (message != null) { @@ -285,7 +283,7 @@ private fun MemberChargeDetailsScreen( endSlot = { when (uiState.paymentDetails.memberCharge.chargeMethod) { MemberPaymentChargeMethod.TRUSTLY, - MemberPaymentChargeMethod.KIVRA, + MemberPaymentChargeMethod.INVOICE, -> { val textToShow: String = if (uiState.paymentDetails.memberCharge.chargeMethod == MemberPaymentChargeMethod.TRUSTLY) { @@ -360,7 +358,7 @@ private fun MemberChargeDetailsScreen( when (val chargeMethod = uiState.paymentDetails.memberCharge.chargeMethod) { MemberPaymentChargeMethod.TRUSTLY, - MemberPaymentChargeMethod.KIVRA, + MemberPaymentChargeMethod.INVOICE, -> { HorizontalItemsWithMaximumSpaceTaken( startSlot = { @@ -369,7 +367,7 @@ private fun MemberChargeDetailsScreen( endSlot = { val text = when (chargeMethod) { MemberPaymentChargeMethod.TRUSTLY -> stringResource(Res.string.PAYMENTS_AUTOGIRO_LABEL) - MemberPaymentChargeMethod.KIVRA -> stringResource(Res.string.PAYMENTS_INVOICE) + MemberPaymentChargeMethod.INVOICE -> stringResource(Res.string.PAYMENTS_INVOICE) else -> "" } HedvigText( @@ -483,7 +481,7 @@ private fun PaymentDetailsScreenPreview( PaymentDetails( memberCharge = when (withPaymentInfo) { TripleCase.FIRST -> paymentDetailsPreviewData - TripleCase.SECOND -> paymentDetailsKivraPreviewData + TripleCase.SECOND -> paymentDetailsINVOICEPreviewData TripleCase.THIRD -> paymentDetailsPreviewData }, pastCharges = chargeHistoryPreviewData, 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..88272f65e2 --- /dev/null +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt @@ -0,0 +1,400 @@ +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.runtime.LaunchedEffect +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.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.hedvig.android.compose.ui.preview.BooleanCollectionPreviewParameterProvider +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.core.uidata.UiCurrencyCode +import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.design.system.hedvig.ButtonDefaults +import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigErrorSection +import com.hedvig.android.design.system.hedvig.HedvigFullScreenCenterAlignedProgress +import com.hedvig.android.design.system.hedvig.HedvigNotificationCard +import com.hedvig.android.design.system.hedvig.HedvigPreview +import com.hedvig.android.design.system.hedvig.HedvigScaffold +import com.hedvig.android.design.system.hedvig.HedvigText +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.HorizontalDivider +import com.hedvig.android.design.system.hedvig.Icon +import com.hedvig.android.design.system.hedvig.NotificationDefaults +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.hedvigDropShadow +import com.hedvig.android.design.system.hedvig.icon.HedvigIcons +import com.hedvig.android.design.system.hedvig.icon.WarningFilled +import com.hedvig.android.design.system.hedvig.rememberHedvigDateTimeFormatter +import com.hedvig.android.design.system.hedvig.rememberHedvigMonthDateTimeFormatter +import com.hedvig.android.feature.payments.data.ManualChargeInfo +import hedvig.resources.BANK_PAYOUT_METHOD_CARD_TITLE +import hedvig.resources.GENERAL_ERROR_BODY +import hedvig.resources.GENERAL_RETRY +import hedvig.resources.MANUAL_CHARGE_CANCELLATION_WARNING +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.SELF_MANUAL_CHARGE_CHANGES_BEEN_MADE_TITLE +import hedvig.resources.claim_status_detail_chat_button_description +import hedvig.resources.payment_details_receipt_card_total +import hedvig.resources.something_went_wrong +import kotlinx.datetime.LocalDate +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun ManualChargeDestination( + viewModel: ManualChargeViewModel, + navigateUp: () -> Unit, + onNavigateToPaymentDetails: (chargeId: String) -> Unit, + onNavigateToSuccess: (Boolean) -> Unit, + openConversation: () -> Unit, +) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle() + + ManualChargeScreen( + uiState = uiState.value, + navigateUp = navigateUp, + reload = { viewModel.emit(ManualChargeEvent.Retry) }, + onNavigateToPaymentDetails = onNavigateToPaymentDetails, + onNavigateToSuccess = { showCancellationWarning -> + viewModel.emit(ManualChargeEvent.ClearNav) + onNavigateToSuccess(showCancellationWarning) + }, + onTriggerPayment = { + viewModel.emit(ManualChargeEvent.TriggerCharge) + }, + openConversation = openConversation, + ) +} + +@Composable +private fun ManualChargeScreen( + uiState: ManualChargeUiState, + navigateUp: () -> Unit, + reload: () -> Unit, + openConversation: () -> Unit, + onNavigateToPaymentDetails: (chargeId: String) -> Unit, + onNavigateToSuccess: (Boolean) -> Unit, + onTriggerPayment: () -> Unit, +) { + HedvigScaffold( + navigateUp = navigateUp, + topAppBarText = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_TITLE), + ) { + when (uiState) { + + is ManualChargeUiState.Failure -> { + val title = if (uiState.error.message != null) + stringResource(Res.string.SELF_MANUAL_CHARGE_CHANGES_BEEN_MADE_TITLE) else + stringResource(Res.string.something_went_wrong) + val subTitle = if (uiState.error.message != null) uiState.error.message else + stringResource(Res.string.GENERAL_ERROR_BODY) + val buttonText = if (uiState.error.message != null) + stringResource(Res.string.claim_status_detail_chat_button_description) else + stringResource(Res.string.GENERAL_RETRY) + val onButtonClick = if (uiState.error.message != null) openConversation else reload + + HedvigErrorSection( + onButtonClick = onButtonClick, + Modifier + .weight(1f) + .fillMaxWidth(), + subTitle = subTitle, + buttonText = buttonText, + title = title, + ) + + } + + ManualChargeUiState.Loading -> { + HedvigFullScreenCenterAlignedProgress( + modifier = Modifier.weight(1f), + ) + } + + is ManualChargeUiState.Success -> { + if (uiState.navigateToSuccess != null) { + LaunchedEffect(uiState.navigateToSuccess) { + onNavigateToSuccess(uiState.manualChargeInfo.showCancellationWarning) + } + } else { + ManualChargeSuccessScreen( + uiState, + onNavigateToPaymentDetails = onNavigateToPaymentDetails, + onTriggerPayment = onTriggerPayment, + ) + } + } + } + } +} + +@Composable +private fun ManualChargeSuccessScreen( + uiState: ManualChargeUiState.Success, + onNavigateToPaymentDetails: (chargeId: String) -> Unit, + onTriggerPayment: () -> Unit, +) { + val dateTimeFormatter = rememberHedvigMonthDateTimeFormatter() + val dateTimeFormatterWithYear = rememberHedvigDateTimeFormatter() + Column { + + + Column( + modifier = Modifier + .padding( + top = 8.dp, + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ) + .hedvigDropShadow(HedvigTheme.shapes.cornerXLarge) + .fillMaxWidth() + .background( + color = HedvigTheme.colorScheme.backgroundPrimary, + shape = HedvigTheme.shapes.cornerXLarge, + ) + .border( + width = 1.dp, + color = HedvigTheme.colorScheme.borderPrimary, + shape = HedvigTheme.shapes.cornerXLarge, + ) + + .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 = stringResource( + Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_SINCE, + dateTimeFormatter.format(uiState.manualChargeInfo.missedDueDate), + ), + ) + HedvigText( + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_BODY), + color = HedvigTheme.colorScheme.textSecondary, + ) + } + } + if (uiState.manualChargeInfo.chargeId != null) { + HedvigButton( + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_VIEW_DETAILS), + onClick = { + onNavigateToPaymentDetails(uiState.manualChargeInfo.chargeId) + }, + enabled = true, + modifier = Modifier.fillMaxWidth(), + buttonStyle = ButtonDefaults.ButtonStyle.Ghost, + buttonSize = ButtonDefaults.ButtonSize.Medium, + border = HedvigTheme.colorScheme.borderPrimary, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + HedvigText( + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_DUE_DATE), + color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label, + ) + HedvigText( + text = dateTimeFormatterWithYear.format(uiState.manualChargeInfo.missedDueDate), + color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label, + ) + } + if (uiState.manualChargeInfo.bankDescriptor != null + ) { + Spacer(Modifier.height(10.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + HedvigText( + text = stringResource(Res.string.BANK_PAYOUT_METHOD_CARD_TITLE), + color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label, + ) + HedvigText( + text = uiState.manualChargeInfo.bankDescriptor, + color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label, + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider() + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + HedvigText( + text = stringResource(Res.string.payment_details_receipt_card_total), + ) + HedvigText( + text = uiState.manualChargeInfo.amountDue.toString(), + textAlign = TextAlign.End, + ) + } + + HedvigButton( + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_PAY, uiState.manualChargeInfo.amountDue), + onClick = onTriggerPayment, + enabled = true, + buttonSize = ButtonDefaults.ButtonSize.Medium, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(8.dp)) + HedvigText( + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_DETAILS_FINE_PRINT), + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + textAlign = TextAlign.Center, + style = HedvigTheme.typography.label, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + if (uiState.manualChargeInfo.showCancellationWarning) { + HedvigNotificationCard( + message = stringResource(Res.string.MANUAL_CHARGE_CANCELLATION_WARNING), + priority = NotificationDefaults.NotificationPriority.AttentionRound, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(Modifier.height(16.dp)) + } + } +} + +@Composable +@Preview +@HedvigPreview +private fun ManualChargeScreenSuccessPreview( + @PreviewParameter( + BooleanCollectionPreviewParameterProvider::class, + ) showCancellationWarning: Boolean, +) { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + ManualChargeScreen( + uiState = ManualChargeUiState.Success( + ManualChargeInfo( + missedDueDate = LocalDate(2026, 1, 15), + amountDue = UiMoney(100.0, UiCurrencyCode.SEK), + chargeId = "chargeId", + bankDescriptor = "**** 8324", + bankAccountDisplayValue = "Swedbank", + showCancellationWarning = showCancellationWarning, + ), + navigateToSuccess = null, + ), + navigateUp = {}, + reload = {}, + {}, + {}, + {}, + {}, + ) + } + } +} + +@Composable +@Preview +@HedvigPreview +private fun ManualChargeScreenLoadingPreview() { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + ManualChargeScreen( + uiState = ManualChargeUiState.Loading, + navigateUp = {}, + reload = {}, + {}, + {}, + {}, + {}, + ) + } + } +} + +@Composable +@Preview +@HedvigPreview +private fun ManualChargeScreenFailurePreview( + @PreviewParameter( + BooleanCollectionPreviewParameterProvider::class, + ) hasUserError: Boolean, +) { + HedvigTheme { + Surface { + ManualChargeScreen( + uiState = ManualChargeUiState.Failure( + ErrorMessage( + message = if (hasUserError) "Cannot charge the failed payment since there have been some changes. " + + "The new amount will be included in the upcoming payment." else null, + ), + ), + 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..75aae2975b --- /dev/null +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt @@ -0,0 +1,106 @@ +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.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.dropUnlessResumed +import com.hedvig.android.compose.ui.preview.BooleanCollectionPreviewParameterProvider +import com.hedvig.android.design.system.hedvig.ButtonDefaults +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize.Large +import com.hedvig.android.design.system.hedvig.EmptyState +import com.hedvig.android.design.system.hedvig.EmptyStateDefaults +import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateButtonStyle.NoButton +import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateIconStyle.SUCCESS +import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigNotificationCard +import com.hedvig.android.design.system.hedvig.HedvigPreview +import com.hedvig.android.design.system.hedvig.HedvigTextButton +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.NotificationDefaults +import com.hedvig.android.design.system.hedvig.Surface +import hedvig.resources.MANUAL_CHARGE_CANCELLATION_WARNING +import hedvig.resources.PAYMENTS_PAYMENT_IN_PROGRESS +import hedvig.resources.PAYMENTS_PAYMENT_IN_PROGRESS_DESCRIPTION +import hedvig.resources.Res +import hedvig.resources.general_close_button +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun ManualChargeSuccessDestination( + showCancellationWarning: Boolean, + 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 = if (showCancellationWarning) + EmptyStateDefaults.EmptyStateIconStyle.SUCCESS_WITH_WARNING + else SUCCESS, + buttonStyle = NoButton, + ) + Column(Modifier.weight(1f)) { + if (showCancellationWarning) { + Spacer(Modifier.height(16.dp)) + HedvigNotificationCard( + message = stringResource(Res.string.MANUAL_CHARGE_CANCELLATION_WARNING), + priority = NotificationDefaults.NotificationPriority.AttentionRound, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(16.dp)) + } + } + HedvigButton( + stringResource(Res.string.general_close_button), + onClick = dropUnlessResumed { popBackStack() }, + buttonSize = Large, + modifier = Modifier.fillMaxWidth(), + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Secondary + ) + Spacer(Modifier.height(16.dp)) + } +} + +@HedvigPreview +@Composable +private fun ManualChargeSuccessDestinationPreview( + @PreviewParameter( + BooleanCollectionPreviewParameterProvider::class, + ) showCancellationWarning: Boolean, +) { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + ManualChargeSuccessDestination( + showCancellationWarning, + {}, + ) + } + } +} diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt new file mode 100644 index 0000000000..ecdf185baa --- /dev/null +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt @@ -0,0 +1,105 @@ +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 +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( + getManualChargeInfoUseCase: GetManualChargeInfoUseCase, + triggerManualCharge: TriggerManualChargeUseCase +) : MoleculeViewModel( + initialState = ManualChargeUiState.Loading, + presenter = ManualChargePresenter(getManualChargeInfoUseCase, triggerManualCharge), +) + +private class ManualChargePresenter( + private val getManualChargeInfoUseCase: GetManualChargeInfoUseCase, + private val triggerManualCharge: TriggerManualChargeUseCase +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present( + lastState: ManualChargeUiState, + ): ManualChargeUiState { + 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().fold( + ifLeft = { + screenState = ManualChargeUiState.Failure(it) + }, + ifRight = { + screenState = ManualChargeUiState.Success( + manualChargeInfo = currentState.manualChargeInfo, + navigateToSuccess = Unit + ) + } + ) + } + } + + LaunchedEffect(dataLoadIteration) { + screenState = ManualChargeUiState.Loading + getManualChargeInfoUseCase.invoke().fold( + ifRight = { manualChargeInfo -> + screenState = ManualChargeUiState.Success( + manualChargeInfo = manualChargeInfo, + null, + ) + }, + ifLeft = { failure -> + screenState = ManualChargeUiState.Failure(failure) + }, + ) + } + return screenState + } +} + +internal sealed interface ManualChargeUiState { + data object Loading : ManualChargeUiState + + data class Failure( + val error: ErrorMessage + ) : ManualChargeUiState + + data class Success( + val manualChargeInfo: ManualChargeInfo, + val navigateToSuccess: Unit? + ) : ManualChargeUiState +} + +internal sealed interface ManualChargeEvent { + data object Retry : ManualChargeEvent + + data object TriggerCharge : ManualChargeEvent + data object ClearNav : ManualChargeEvent +} + 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 e5472084a0..915c794056 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt @@ -3,6 +3,8 @@ 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.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -24,12 +26,14 @@ 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 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 @@ -39,8 +43,11 @@ 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 +import com.hedvig.android.design.system.hedvig.HedvigButton import com.hedvig.android.design.system.hedvig.HedvigCard import com.hedvig.android.design.system.hedvig.HedvigErrorSection import com.hedvig.android.design.system.hedvig.HedvigInformationSection @@ -51,20 +58,24 @@ 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 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 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.icon.PaymentOutline 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 @@ -89,6 +100,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.PAYOUT_PAGE_HEADING @@ -114,6 +129,7 @@ internal fun PaymentsDestination( onPayoutAccountClicked: () -> Unit, onMemberPaymentDetailsClicked: () -> Unit, onChangeBankAccount: () -> Unit, + onOpenManualCharge: () -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() PaymentsScreen( @@ -125,6 +141,7 @@ internal fun PaymentsDestination( onPayoutAccountClicked = onPayoutAccountClicked, onRetry = { viewModel.emit(Retry) }, onPaymentDetailsClicked = onMemberPaymentDetailsClicked, + onOpenManualCharge = onOpenManualCharge, ) } @@ -137,6 +154,7 @@ private fun PaymentsScreen( onPaymentHistoryClicked: () -> Unit, onPayoutAccountClicked: () -> Unit, onPaymentDetailsClicked: () -> Unit, + onOpenManualCharge: () -> Unit, onRetry: () -> Unit, ) { val density = LocalDensity.current @@ -199,6 +217,7 @@ private fun PaymentsScreen( onPaymentHistoryClicked = onPaymentHistoryClicked, onPayoutAccountClicked = onPayoutAccountClicked, onPaymentDetailsClicked = onPaymentDetailsClicked, + onOpenManualCharge = onOpenManualCharge, ) Spacer(Modifier.height(16.dp)) } @@ -224,6 +243,7 @@ private fun PaymentsContent( onPaymentHistoryClicked: () -> Unit, onPayoutAccountClicked: () -> Unit, onPaymentDetailsClicked: () -> Unit, + onOpenManualCharge: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -232,6 +252,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( @@ -242,12 +276,14 @@ private fun PaymentsContent( } val upcomingPayment = (uiState as? Content)?.upcomingPayment if (upcomingPayment == NoUpcomingPayment) { - HedvigInformationSection( - stringResource(Res.string.PAYMENTS_NO_PAYMENTS_IN_PROGRESS), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) + if (ongoingCharges.isNullOrEmpty()) { + HedvigInformationSection( + stringResource(Res.string.PAYMENTS_NO_PAYMENTS_IN_PROGRESS), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } } else { PaymentAmountCard( upcomingPayment = upcomingPayment as? UpcomingPayment.Content, @@ -342,7 +378,10 @@ private fun CardNotConnectedWarningCard( } @Composable -private fun UpcomingPaymentInfoCard(upcomingPaymentInfo: UpcomingPaymentInfo?, modifier: Modifier = Modifier) { +private fun UpcomingPaymentInfoCard( + upcomingPaymentInfo: UpcomingPaymentInfo?, + modifier: Modifier = Modifier, +) { Box(modifier) { when (upcomingPaymentInfo) { NoInfo -> {} @@ -356,14 +395,19 @@ 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), - ), - ) + 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, + ) + } + } } null -> {} @@ -575,6 +619,84 @@ private fun PaymentCard( } } +@Composable +private fun FailedPaymentInfo(amountDue: String, onReviewPaymentClick: () -> Unit, modifier: Modifier = Modifier) { + HedvigCard( + color = HedvigTheme.colorScheme.fillNegative, + modifier = modifier + .fillMaxWidth() + .border(1.dp, HedvigTheme.colorScheme.borderPrimary, + HedvigTheme.shapes.cornerXLarge) + .hedvigDropShadow() + ) { + 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( + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_TITLE), + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textPrimary, + ) + HedvigText( + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_AMOUNT_DUE, amountDue), + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondary, + ) + } + } + Spacer(Modifier.height(8.dp)) + HedvigText( + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_BODY), + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondary, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + + Spacer(Modifier.height(12.dp)) + HedvigButton( + text = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_BUTTON), + onClick = onReviewPaymentClick, + enabled = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + buttonSize = ButtonDefaults.ButtonSize.Small, + ) + } + } +} + @Composable private fun PaymentsListItem( text: String, @@ -598,6 +720,20 @@ private fun PaymentsListItem( ) } +@Composable +@HedvigPreview +private fun PreviewFailedPaymentInfo() { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + FailedPaymentInfo( + amountDue = "233 kr", + {}, + ) + } + } +} + + @Composable @HedvigPreview private fun PreviewPaymentScreen( @@ -614,6 +750,7 @@ private fun PreviewPaymentScreen( {}, {}, {}, + {}, ) } } @@ -665,13 +802,16 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< Content( isRetrying = false, upcomingPayment = UpcomingPayment.Content( - UiMoney(100.0, SEK), + UiMoney(400.0, SEK), System.now().toLocalDateTime(TimeZone.UTC).date, "pwe", ), upcomingPaymentInfo = PaymentFailed( System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, + isManualChargeAllowed = ManualChargeToPrompt( + UiMoney(200.0, UiCurrencyCode.SEK), + ), ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.Active, @@ -700,7 +840,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 = null, + ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( null, @@ -735,6 +879,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< upcomingPaymentInfo = PaymentFailed( System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, + isManualChargeAllowed = null, ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( @@ -754,6 +899,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< upcomingPaymentInfo = PaymentFailed( System.now().toLocalDateTime(TimeZone.UTC).date, System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, + isManualChargeAllowed = null, ), 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 a2108c8588..3e0ede1f47 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt @@ -11,8 +11,10 @@ import arrow.core.Either import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.demomode.Provider import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.feature.payments.data.ManualChargeToPrompt import com.hedvig.android.feature.payments.data.MemberCharge import com.hedvig.android.feature.payments.data.PaymentConnection +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 @@ -24,8 +26,10 @@ import com.hedvig.android.feature.payments.overview.data.GetUpcomingPaymentUseCa import com.hedvig.android.feature.payments.ui.payments.PaymentsUiState.Content.ConnectedPaymentInfo import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope +import kotlinx.coroutines.flow.collectLatest import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.firstOrNull import kotlinx.datetime.LocalDate internal class PaymentsPresenter( @@ -75,6 +79,7 @@ internal class PaymentsPresenter( return@run PaymentsUiState.Content.UpcomingPaymentInfo.PaymentFailed( failedPaymentStartDate = failedCharge.fromDate, failedPaymentEndDate = failedCharge.toDate, + isManualChargeAllowed = paymentOverview.isManualChargeAllowed, ) } PaymentsUiState.Content.UpcomingPaymentInfo.NoInfo @@ -160,6 +165,7 @@ internal sealed interface PaymentsUiState { data class PaymentFailed( val failedPaymentStartDate: LocalDate, val failedPaymentEndDate: LocalDate, + 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..54ec60fbbe 100644 --- a/app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/flags/Feature.kt +++ b/app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/flags/Feature.kt @@ -21,5 +21,7 @@ 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") } 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 17b59f672a..d8aa749936 100644 --- a/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt +++ b/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt @@ -80,6 +80,8 @@ interface HedvigDeepLinkContainer { val petIdWithoutContractId: List val petIdWithContractId: List + + val manualCharge: List } internal class HedvigDeepLinkContainerImpl( @@ -198,6 +200,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" + } } val HedvigDeepLinkContainer.allDeepLinkUriPatterns: List @@ -228,6 +234,7 @@ val HedvigDeepLinkContainer.allDeepLinkUriPatterns: List inbox.first(), insuranceEvidence.first(), insurances.first(), + manualCharge.first(), moveContract.first(), payments.first(), payout.first(), 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..738bfdbd7b 100644 --- a/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/di/NotificationBadgeModule.kt +++ b/app/notification-badge-data/notification-badge-data-public/src/main/kotlin/com/hedvig/android/notification/badge/data/di/NotificationBadgeModule.kt @@ -4,12 +4,18 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import com.apollographql.apollo.ApolloClient import com.hedvig.android.core.demomode.DemoManager +import com.hedvig.android.featureflags.FeatureManager import com.hedvig.android.notification.badge.data.crosssell.CrossSellNotificationBadgeService import com.hedvig.android.notification.badge.data.crosssell.GetCrossSellRecommendationIdUseCase import com.hedvig.android.notification.badge.data.crosssell.GetCrossSellRecommendationIdUseCaseImpl 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.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 import com.hedvig.android.notification.badge.data.storage.NotificationBadgeStorage import org.koin.dsl.module @@ -33,4 +39,18 @@ val notificationBadgeModule = module { single { CrossSellHomeNotificationServiceImpl(get(), get>()) } + + single { + MissedPaymentNotificationServiceProvider( + demoManager = get(), + demoImpl = DemoMissedPaymentNotificationService(), + prodImpl = get(), + ) + } + single { + GetIfMissedPaymentUseCaseImpl(get(), 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/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..fb27d75a0e --- /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,70 @@ +package com.hedvig.android.notification.badge.data.payment + +import arrow.core.raise.context.raise +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.cache.normalized.FetchPolicy +import com.apollographql.apollo.cache.normalized.fetchPolicy +import com.hedvig.android.apollo.safeFlow +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.Feature +import com.hedvig.android.logger.logcat +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +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, + private val featureManager: FeatureManager +) : GetIfMissedPaymentUseCase { + override fun invoke(): Flow { + return flow { + val isFeatureFlagOn = featureManager.isFeatureEnabled(Feature.ENABLE_MANUAL_CHARGE).firstOrNull() ?: false + if (!isFeatureFlagOn) { + logcat { "ENABLE_MANUAL_CHARGE flag is off" } + emit(false) + return@flow + } + + while (currentCoroutineContext().isActive) { + val hasMissedPayment = apolloClient + .query(MissedPaymentQuery()) + .fetchPolicy(FetchPolicy.CacheAndNetwork) + .safeFlow { + logcat { "GetIfMissedPaymentUseCaseImpl error: $it" } + ErrorMessage() + } + .map { result -> + result.fold( + { + logcat { "GetIfMissedPaymentUseCaseImpl: error when loading missed payment: $it" } + false + }, + { data -> + data.currentMember.missedChargeIdToChargeManually != null + }, + ) + } + .firstOrNull() ?: false + + emit(hasMissedPayment) + + if (!hasMissedPayment) { + break + } + + delay(15.seconds) + } + } + } +} 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..0000e0ffb0 --- /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 getIfMissedPaymentUseCase: GetIfMissedPaymentUseCase, +) : MissedPaymentNotificationService { + override fun showRedDotNotification(): Flow { + return getIfMissedPaymentUseCase.invoke() + } +}