diff --git a/CLAUDE.md b/CLAUDE.md index 71031c0c27..daedd47cb0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -194,6 +194,7 @@ val applicationModule = module { - Use `Provider` when we need a different implementation for the demo mode of the App, which we very rarely do. We always do that using `ProdOrDemoProvider` - Each feature/data module has its own DI module - Common dependencies (logging, tracking) auto-injected by build plugin +- When a Presenter or ViewModel needs to call a use case, always inject the use case directly as a typed dependency — never abstract it into an anonymous `suspend () -> T` lambda. If two separate operations are needed (e.g. payin vs payout setup), create two separate, dedicated use case classes and two separate presenters. Do not create a shared interface just to enable reuse through a single presenter. ### Data Layer diff --git a/app/apollo/apollo-network-cache-manager/build.gradle.kts b/app/apollo/apollo-network-cache-manager/build.gradle.kts index ab91ad2ff0..dceb1aaaae 100644 --- a/app/apollo/apollo-network-cache-manager/build.gradle.kts +++ b/app/apollo/apollo-network-cache-manager/build.gradle.kts @@ -1,10 +1,14 @@ plugins { - id("hedvig.jvm.library") + id("hedvig.multiplatform.library") id("hedvig.gradle.plugin") } -dependencies { - implementation(libs.apollo.normalizedCache) - implementation(libs.apollo.runtime) - implementation(libs.koin.core) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.apollo.normalizedCache) + implementation(libs.apollo.runtime) + implementation(libs.koin.core) + } + } } diff --git a/app/apollo/apollo-network-cache-manager/src/main/kotlin/com/hedvig/android/apollo/NetworkCacheManager.kt b/app/apollo/apollo-network-cache-manager/src/commonMain/kotlin/com/hedvig/android/apollo/NetworkCacheManager.kt similarity index 100% rename from app/apollo/apollo-network-cache-manager/src/main/kotlin/com/hedvig/android/apollo/NetworkCacheManager.kt rename to app/apollo/apollo-network-cache-manager/src/commonMain/kotlin/com/hedvig/android/apollo/NetworkCacheManager.kt diff --git a/app/apollo/apollo-network-cache-manager/src/main/kotlin/com/hedvig/android/apollo/di/NetworkCacheManagerModule.kt b/app/apollo/apollo-network-cache-manager/src/commonMain/kotlin/com/hedvig/android/apollo/di/NetworkCacheManagerModule.kt similarity index 100% rename from app/apollo/apollo-network-cache-manager/src/main/kotlin/com/hedvig/android/apollo/di/NetworkCacheManagerModule.kt rename to app/apollo/apollo-network-cache-manager/src/commonMain/kotlin/com/hedvig/android/apollo/di/NetworkCacheManagerModule.kt diff --git a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls index 288983627b..c22e8b1420 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 @@ -1728,6 +1728,11 @@ type ExternalInsurer { displayName: String! insurelyId: String } +type FetchedExternalInsurance { + displayName: String! + subtitle: String + insurer: ExternalInsurer! +} type FirstVetAction { sections: [FirstVetSection!]! } @@ -2729,6 +2734,22 @@ type MemberPaymentAvailablePaymentMethod { True if the member can set up this payment method for payout. """ supportsPayout: Boolean! + """ + True if this method is already ACTIVE for member and can be chosen as default directly without setup, false if + this is a new payment method that the member has not yet set up. + If this is true, then the `details` field will be populated with the payment method details. If this is false, then + the `details` field will be null since the member has not yet set up this payment method. + If true then this method can be set up as default directly by calling `paymentMethodSetDefaultPayin` or + `paymentMethodSetDefaultPayout` mutation depending on if it's a payin or payout method. If false, then the + corresponding mutation for setting up this payment method should be called, eg. `paymentMethodSetupTrustly`, + `paymentMethodSetupSwishPayin` etc. + """ + isActive: Boolean! + """ + For already connected and ACTIVE methods, ie isActive=true, specific details of the actual connection - e.g. a bank + account reference, phone number for swish, or email/kivra for invoice. + """ + details: PaymentMethodDetails } type MemberPaymentChargeMethodInfo { """ @@ -2801,48 +2822,75 @@ type MemberPaymentInformation { } type MemberPaymentMethod { """ - The unique id of the payment method. This id is used for switching default and revoking payment methods. - """ - id: ID! - """ - Payment provider, eg Trustly, Swish, Nordea, Kivra etc. + Payment provider, eg Trustly, Swish, Nordea, Kivra etc. + This is used as the "identifier" of the payment method since there can only be one ACTIVE or PENDING payment method + per provider. """ provider: MemberPaymentProvider! """ - The payment method status - ACTIVE, PENDING, or PENDING_DEFAULT. - PENDING_DEFAULT means the payment method is awaiting activation and will become default once activated. + The payment method status - ACTIVE, PENDING. + If ACTIVE, the payment method is ready to use for payins or payouts depending on if it's a payin or payout method. + If PENDING, the payment method has been set up but is still awaiting activation and cannot be used for payins or + payouts until then. Once activated, the status will change to ACTIVE. """ status: MemberPaymentMethodStatus! """ - True if this is the default payment method. Only one ACTIVE payment method can be default at a time. - If status is PENDING then payment method will become the default once activated. + This is 'true' for only one of the members ACTIVE methods which is the default payment method that will be used for + charging or payout the member. For PENDING methods, this can also be 'true' if the member has chosen to set up this + payment method as default during the setup process. """ isDefault: Boolean! """ - Specific details of the actual connection - e.g. a bank account reference, phone number for swish, + Specific details of the actual connection if method is ACTIVE - e.g. a bank account reference, phone number for swish, or email/kivra for invoice. """ details: PaymentMethodDetails! } type MemberPaymentMethods { """ - List of active and pending payment payin methods for this member. + List of all member's ACTIVE and PENDING payment payin methods. + A member can have multiple ACTIVE payment methods with these constraints: + - Only one ACTIVE payment method per provider, eg. one Trustly, one Swish, one Nordea etc. + - Only one ACTIVE payment method can be default at a time. + A member can have multiple PENDING payment methods with these constraints: + - Only one PENDING payment method per provider, eg. one Trustly, one Swish, one Nordea etc. + So there can exist max two payment methods per provider, one ACTIVE and one PENDING. + If a PENDING payment method has isDefault=true, then it will become the default ACTIVE payment method once activated. """ payinMethods: [MemberPaymentMethod!]! """ - List of active and pending payment payout methods for this member. + List of all member's ACTIVE and PENDING payment payout methods. + A member can have multiple ACTIVE payment methods with these constraints: + - Only one ACTIVE payment method per provider, eg. one Trustly, one Swish, one Nordea etc. + - Only one ACTIVE payment method can be default at a time. + A member can have multiple PENDING payment methods with these constraints: + - Only one PENDING payment method per provider, eg. one Trustly, one Swish, one Nordea etc. + So there can exist max two payment methods per provider, one ACTIVE and one PENDING. + If a PENDING payment method has isDefault=true, then it will become the default ACTIVE payment method once activated. """ payoutMethods: [MemberPaymentMethod!]! """ - The default payment method for payin if any. + The default payment method to use for payins if any. + Note that there can exist a PENDING payment method in `payinMethods` list with `isDefault`=true, in that case this default + payment method will be replaced by it once the pending method is activated. """ defaultPayinMethod: MemberPaymentMethod """ - The default payment method for payout if any. + The default payment method to use for payouts if any. + Note that there can exist a PENDING payment method in `payoutMethods` list with `isDefault`=true, in that case this default + payment method will be replaced by it once the pending method is activated. """ defaultPayoutMethod: MemberPaymentMethod """ - The available payment methods that the member can choose from when setting up a new payment method. + The available payment methods that the member can choose from when setting up a new payment method. + This list can include both payment methods that the member has already set up and new payment methods that the + member has not yet set up but are available to them. For already set up payment methods, the `isActive` field will + be true and the `details` field will be populated with the payment method details. For new payment methods that the + member has not yet set up, the `isActive` field will be false and the `details` field will be null. + If member picks a new payment method to set up, the corresponding mutation for setting up that payment method should + be called, eg. `paymentMethodSetupTrustly`, `paymentMethodSetupSwishPayin` etc. + If member picks an already set up payment method to set up as default, then `paymentMethodSetDefaultPayin` or + `paymentMethodSetDefaultPayout` mutation should be called depending on if it's a payin or payout method. """ availableMethods: [MemberPaymentAvailablePaymentMethod!]! """ @@ -3392,9 +3440,9 @@ type Mutation { """ Setup invoice payment method for the member. Kivra will be used as the provider if supported, else mail. """ - paymentMethodSetupInvoicePayin(input: PaymentMethodSetupInvoicePayinInput!): PaymentMethodSetupOutput! + paymentMethodSetupInvoicePayin: PaymentMethodSetupOutput! """ - Setup Trustly payment payin and payout method for the member. + Setup Trustly payment payin and payout method for the member. Requires member consent via redirect to Trustly URL in response. """ paymentMethodSetupTrustly(input: PaymentMethodSetupTrustlyInput!): PaymentMethodSetupOutput! """ @@ -3406,17 +3454,19 @@ type Mutation { """ paymentMethodSetupSwishPayout(input: PaymentMethodSetupSwishInput!): PaymentMethodSetupOutput! """ - Setup Swish payin method for the member. + Setup Swish payin method for the member. Requires member consent in Swish app. """ paymentMethodSetupSwishPayin(input: PaymentMethodSetupSwishInput!): PaymentMethodSetupOutput! """ - Revoke an active payment method. The member will be required to set up a new payment method if they revoke their default one. + A member can have multiple ACTIVE payment methods where one of those is default. This mutation changes the + members default payment method for charging to any of his/hers other active payment methods. """ - paymentMethodRevoke(id: ID!): UserError + paymentMethodSetDefaultPayin(provider: MemberPaymentProvider!): UserError """ - Set an active payment method as default. + A member can have multiple ACTIVE payment methods where one of those is default. This mutation changes the + members default payment method for payouts to any of his/hers other active payment methods. """ - paymentMethodSetDefault(id: ID!): UserError + paymentMethodSetDefaultPayout(provider: MemberPaymentProvider!): UserError """ Start a conversation. This is effectively creating one, but with two slight differences from a regular "create something"-mutation: @@ -3707,17 +3757,7 @@ type PaymentMethodInvoiceDetails { """ email: String } -input PaymentMethodSetupInvoicePayinInput { - """ - Set up invoice payment method as default. - """ - setAsDefaultPayout: Boolean! -} input PaymentMethodSetupNordeaPayoutInput { - """ - Set up Nordea payout method as default. - """ - setAsDefault: Boolean! """ The clearing number for member's bank account. """ @@ -3756,24 +3796,12 @@ enum PaymentMethodSetupStatus { FAILED } input PaymentMethodSetupSwishInput { - """ - Set up Swish payment method as default. - """ - setAsDefault: Boolean! """ The Swish mobile number to use for payout or payin. """ phoneNumber: String! } input PaymentMethodSetupTrustlyInput { - """ - Set up Trustly payment method as default for payin. - """ - setAsDefaultPayin: Boolean! - """ - Set up Trustly payment method as default for payout. - """ - setAsDefaultPayout: Boolean! """ The URL to redirect the member back to after a successful setup after Trustly onboarding. """ @@ -3905,6 +3933,11 @@ type PriceIntent { When 'true' it means user has gone trough Insurely flow with that price intent """ hasCollectedInsurelyData: Boolean! + """ + List of external insurances fetched via Insurely that correspond to products Hedvig offers. + Null when no Insurely data collection has been associated with this price intent. + """ + fetchedExternalInsurances: [FetchedExternalInsurance!] } enum PriceIntentAnimal { CAT @@ -4234,6 +4267,11 @@ if the user has input enough information to generate it. type ProductRecommendation { product: Product! offer: ProductOffer + """ + External insurance data from Insurely, available even when no offer could be generated. + Null when no Insurely data collection is associated with the session. + """ + externalInsurance: RecommendationExternalInsurance } type ProductVariant { """ @@ -4366,6 +4404,32 @@ type Query { """ addonOfferCost(quoteId: ID!, selectedAddonIds: [ID!]!): ItemCost! } +type RecommendationExternalInsurance { + """ + Display name of the external insurance product + """ + displayName: String! + """ + The external insurer + """ + insurer: ExternalInsurer! + """ + Monthly price of the external insurance. Null if not available. + """ + price: Money + """ + Contextual subtitle (e.g. address, registration number, pet name) + """ + subtitle: String + """ + Renewal date of the external policy. Null when not provided. + """ + renewalDate: Date + """ + Insurely data collection ID + """ + dataCollectionId: String! +} type RecommendedCrossSell { crossSell: CrossSell! bannerText: String! diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index f5f26cf90a..6f11b9e455 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -207,6 +207,7 @@ dependencies { implementation(projects.featureMovingflow) implementation(projects.featureRemoveAddons) + implementation(projects.featurePayoutAccount) implementation(projects.featurePayments) implementation(projects.featureProfile) implementation(projects.featureTerminateInsurance) diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/di/ApplicationModule.kt b/app/app/src/main/kotlin/com/hedvig/android/app/di/ApplicationModule.kt index b4d78717c5..ff9b0703c5 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/di/ApplicationModule.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/di/ApplicationModule.kt @@ -79,6 +79,7 @@ import com.hedvig.android.feature.insurances.di.insurancesModule import com.hedvig.android.feature.login.di.loginModule import com.hedvig.android.feature.movingflow.di.movingFlowModule import com.hedvig.android.feature.payments.di.paymentsModule +import com.hedvig.android.feature.payoutaccount.di.payoutAccountModule import com.hedvig.android.feature.profile.di.profileModule import com.hedvig.android.feature.terminateinsurance.di.terminateInsuranceModule import com.hedvig.android.feature.travelcertificate.di.travelCertificateModule @@ -344,6 +345,7 @@ val applicationModule = module { networkModule, notificationBadgeModule, notificationModule, + payoutAccountModule, paymentsModule, profileModule, settingsDatastoreModule, 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 43fdab75f4..04505af23c 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 @@ -36,6 +36,7 @@ import com.hedvig.android.feature.claimhistory.nav.ClaimHistoryDestination import com.hedvig.android.feature.claimhistory.nav.claimHistoryGraph import com.hedvig.android.feature.connect.payment.connectPaymentGraph import com.hedvig.android.feature.connect.payment.trustly.ui.TrustlyDestination +import com.hedvig.android.feature.connect.payment.trustly.ui.TrustlyPayoutDestination import com.hedvig.android.feature.deleteaccount.navigation.DeleteAccountDestination import com.hedvig.android.feature.deleteaccount.navigation.deleteAccountGraph import com.hedvig.android.feature.editcoinsured.navigation.EditCoInsuredDestination.CoInsuredAddInfo @@ -68,6 +69,8 @@ import com.hedvig.android.feature.login.navigation.loginGraph import com.hedvig.android.feature.movingflow.SelectContractForMoving import com.hedvig.android.feature.movingflow.movingFlowGraph import com.hedvig.android.feature.payments.navigation.paymentsGraph +import com.hedvig.android.feature.payoutaccount.navigation.PayoutAccountDestination +import com.hedvig.android.feature.payoutaccount.navigation.payoutAccountGraph import com.hedvig.android.feature.profile.navigation.ProfileDestination import com.hedvig.android.feature.profile.tab.profileGraph import com.hedvig.android.feature.terminateinsurance.navigation.TerminateInsuranceGraphDestination @@ -339,10 +342,16 @@ internal fun HedvigNavHost( navController = navController, hedvigDeepLinkContainer = hedvigDeepLinkContainer, navigateToConnectPayment = navigateToConnectPayment, + navigateToPayoutAccount = { navController.navigate(PayoutAccountDestination.Graph) }, languageService = languageService, hedvigBuildConstants = hedvigBuildConstants, onOpenChat = ::navigateToNewConversation, ) + payoutAccountGraph( + navController = navController, + navigateToTrustlyPayout = { navController.navigate(TrustlyPayoutDestination) }, + navigateUp = navController::navigateUp, + ) profileGraph( settingsDestinationNestedGraphs = { deleteAccountGraph(hedvigDeepLinkContainer, navController) diff --git a/app/feature/feature-connect-payment-trustly/src/main/graphql/SetupTrustlyPayout.graphql b/app/feature/feature-connect-payment-trustly/src/main/graphql/SetupTrustlyPayout.graphql new file mode 100644 index 0000000000..5ecb8535cc --- /dev/null +++ b/app/feature/feature-connect-payment-trustly/src/main/graphql/SetupTrustlyPayout.graphql @@ -0,0 +1,14 @@ +mutation SetupTrustlyPayout($successUrl: String!, $failureUrl: String!) { + paymentMethodSetupTrustly( + input: { + successUrl: $successUrl, + failureUrl: $failureUrl + } + ) { + status + url + error { + message + } + } +} diff --git a/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/StartTrustlyPayoutSessionUseCase.kt b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/StartTrustlyPayoutSessionUseCase.kt new file mode 100644 index 0000000000..4752f491ad --- /dev/null +++ b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/StartTrustlyPayoutSessionUseCase.kt @@ -0,0 +1,36 @@ +package com.hedvig.android.feature.connect.payment.trustly + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull +import com.apollographql.apollo.ApolloClient +import com.hedvig.android.apollo.ErrorMessage +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.feature.connect.payment.trustly.data.TrustlyCallback +import com.hedvig.android.logger.logcat +import octopus.SetupTrustlyPayoutMutation + +internal class StartTrustlyPayoutSessionUseCase( + private val apolloClient: ApolloClient, + private val trustlyCallback: TrustlyCallback, +) { + suspend fun invoke(): Either { + return either { + val data = apolloClient + .mutation( + SetupTrustlyPayoutMutation( + successUrl = trustlyCallback.successUrl, + failureUrl = trustlyCallback.failureUrl, + ), + ) + .safeExecute(::ErrorMessage) + .bind() + logcat { "StartTrustlyPayoutSessionUseCase received: ${data.paymentMethodSetupTrustly}" } + val url = ensureNotNull(data.paymentMethodSetupTrustly.url) { + ErrorMessage("Trustly payout setup returned no URL") + } + TrustlyInitiateProcessUrl(url) + } + } +} diff --git a/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/TrustlyPayoutPresenter.kt b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/TrustlyPayoutPresenter.kt new file mode 100644 index 0000000000..54da291b63 --- /dev/null +++ b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/TrustlyPayoutPresenter.kt @@ -0,0 +1,88 @@ +package com.hedvig.android.feature.connect.payment.trustly + +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.apollo.NetworkCacheManager +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.core.common.safeCast +import com.hedvig.android.feature.connect.payment.trustly.data.TrustlyCallback +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope + +internal class TrustlyPayoutPresenter( + private val trustlyCallback: TrustlyCallback, + private val startTrustlyPayoutSessionUseCase: StartTrustlyPayoutSessionUseCase, + private val cacheManager: NetworkCacheManager, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present(lastState: TrustlyUiState): TrustlyUiState { + var browsing: TrustlyUiState.Browsing? by remember { + mutableStateOf(lastState.safeCast()) + } + var startSessionError: ErrorMessage? by remember { mutableStateOf(null) } + var connectingCardFailed by remember { mutableStateOf(lastState is TrustlyUiState.FailedToConnectCard) } + var succeededInConnectingCard by remember { mutableStateOf(lastState is TrustlyUiState.SucceededInConnectingCard) } + + var loadIteration by remember { mutableIntStateOf(0) } + + LaunchedEffect(loadIteration) { + if (browsing != null) return@LaunchedEffect + if (startSessionError != null) return@LaunchedEffect + if (connectingCardFailed) return@LaunchedEffect + if (succeededInConnectingCard) return@LaunchedEffect + startTrustlyPayoutSessionUseCase.invoke().fold( + ifLeft = { + startSessionError = it + browsing = null + }, + ifRight = { + startSessionError = null + browsing = TrustlyUiState.Browsing(it.url, trustlyCallback) + }, + ) + } + + CollectEvents { event -> + when (event) { + TrustlyEvent.ConnectingCardFailed -> { + connectingCardFailed = true + } + + TrustlyEvent.ConnectingCardSucceeded -> { + succeededInConnectingCard = true + } + + TrustlyEvent.RetryConnectingCard -> { + browsing = null + startSessionError = null + connectingCardFailed = false + succeededInConnectingCard = false + loadIteration++ + } + } + } + + if (succeededInConnectingCard) { + LaunchedEffect(Unit) { + cacheManager.clearCache() + } + return TrustlyUiState.SucceededInConnectingCard + } + if (connectingCardFailed) { + return TrustlyUiState.FailedToConnectCard + } + if (startSessionError != null) { + return TrustlyUiState.FailedToStartSession + } + val browsingValue = browsing + if (browsingValue != null) { + return browsingValue + } + return TrustlyUiState.Loading + } +} diff --git a/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/TrustlyPayoutViewModel.kt b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/TrustlyPayoutViewModel.kt new file mode 100644 index 0000000000..36ed291cba --- /dev/null +++ b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/TrustlyPayoutViewModel.kt @@ -0,0 +1,18 @@ +package com.hedvig.android.feature.connect.payment.trustly + +import com.hedvig.android.apollo.NetworkCacheManager +import com.hedvig.android.feature.connect.payment.trustly.data.TrustlyCallback +import com.hedvig.android.molecule.public.MoleculeViewModel + +internal class TrustlyPayoutViewModel( + trustlyCallback: TrustlyCallback, + startTrustlyPayoutSessionUseCase: StartTrustlyPayoutSessionUseCase, + networkCacheManager: NetworkCacheManager, +) : MoleculeViewModel( + TrustlyUiState.Loading, + TrustlyPayoutPresenter( + trustlyCallback, + startTrustlyPayoutSessionUseCase, + networkCacheManager, + ), + ) diff --git a/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/di/ConnectPaymentTrustlyModule.kt b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/di/ConnectPaymentTrustlyModule.kt index 9b4f4182f8..a00ecf9cb2 100644 --- a/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/di/ConnectPaymentTrustlyModule.kt +++ b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/di/ConnectPaymentTrustlyModule.kt @@ -3,7 +3,9 @@ package com.hedvig.android.feature.connect.payment.trustly.di import com.apollographql.apollo.ApolloClient import com.hedvig.android.apollo.NetworkCacheManager import com.hedvig.android.core.buildconstants.HedvigBuildConstants +import com.hedvig.android.feature.connect.payment.trustly.StartTrustlyPayoutSessionUseCase import com.hedvig.android.feature.connect.payment.trustly.StartTrustlySessionUseCase +import com.hedvig.android.feature.connect.payment.trustly.TrustlyPayoutViewModel import com.hedvig.android.feature.connect.payment.trustly.TrustlyViewModel import com.hedvig.android.feature.connect.payment.trustly.data.TrustlyCallback import com.hedvig.android.feature.connect.payment.trustly.data.TrustlyCallbackImpl @@ -15,6 +17,9 @@ val connectPaymentTrustlyModule = module { single { StartTrustlySessionUseCase(get(), get()) } + single { + StartTrustlyPayoutSessionUseCase(get(), get()) + } viewModel { TrustlyViewModel( trustlyCallback = get(), @@ -22,4 +27,11 @@ val connectPaymentTrustlyModule = module { networkCacheManager = get(), ) } + viewModel { + TrustlyPayoutViewModel( + trustlyCallback = get(), + startTrustlyPayoutSessionUseCase = get(), + networkCacheManager = get(), + ) + } } diff --git a/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/navigation/ConnectTrustlyPaymentGraph.kt b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/navigation/ConnectTrustlyPaymentGraph.kt index ae0d4d83e0..a63c8eaff6 100644 --- a/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/navigation/ConnectTrustlyPaymentGraph.kt +++ b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/navigation/ConnectTrustlyPaymentGraph.kt @@ -2,8 +2,10 @@ package com.hedvig.android.feature.connect.payment import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder +import com.hedvig.android.feature.connect.payment.trustly.TrustlyPayoutViewModel import com.hedvig.android.feature.connect.payment.trustly.TrustlyViewModel import com.hedvig.android.feature.connect.payment.trustly.ui.TrustlyDestination +import com.hedvig.android.feature.connect.payment.trustly.ui.TrustlyPayoutDestination import com.hedvig.android.navigation.compose.navDeepLinks import com.hedvig.android.navigation.compose.navdestination import com.hedvig.android.navigation.core.HedvigDeepLinkContainer @@ -26,4 +28,13 @@ fun NavGraphBuilder.connectPaymentGraph( finishTrustlyFlow = navController::popBackStack, ) } + + navdestination { + val viewModel: TrustlyPayoutViewModel = koinViewModel() + TrustlyDestination( + viewModel = viewModel, + navigateUp = navController::navigateUp, + finishTrustlyFlow = navController::popBackStack, + ) + } } diff --git a/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/ui/TrustlyDestination.kt b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/ui/TrustlyDestination.kt index f0acc11a1b..3c1c3b90f6 100644 --- a/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/ui/TrustlyDestination.kt +++ b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/ui/TrustlyDestination.kt @@ -41,6 +41,7 @@ import com.hedvig.android.feature.connect.payment.trustly.TrustlyEvent import com.hedvig.android.feature.connect.payment.trustly.TrustlyUiState import com.hedvig.android.feature.connect.payment.trustly.TrustlyViewModel import com.hedvig.android.feature.connect.payment.trustly.data.PreviewTrustlyCallback +import com.hedvig.android.molecule.public.MoleculeViewModel import com.hedvig.android.feature.connect.payment.trustly.sdk.TrustlyWebChromeClient import com.hedvig.android.feature.connect.payment.trustly.sdk.TrustlyWebView import com.hedvig.android.feature.connect.payment.trustly.sdk.TrustlyWebViewClient @@ -58,8 +59,15 @@ import org.jetbrains.compose.resources.stringResource @Serializable data object TrustlyDestination : Destination +@Serializable +data object TrustlyPayoutDestination : Destination + @Composable -internal fun TrustlyDestination(viewModel: TrustlyViewModel, navigateUp: () -> Unit, finishTrustlyFlow: () -> Unit) { +internal fun TrustlyDestination( + viewModel: MoleculeViewModel, + navigateUp: () -> Unit, + finishTrustlyFlow: () -> Unit, +) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() TrustlyScreen( uiState = uiState, diff --git a/app/feature/feature-payments/build.gradle.kts b/app/feature/feature-payments/build.gradle.kts index 763237417d..cd1416f07a 100644 --- a/app/feature/feature-payments/build.gradle.kts +++ b/app/feature/feature-payments/build.gradle.kts @@ -1,5 +1,6 @@ plugins { - id("hedvig.android.library") + id("hedvig.multiplatform.library") + id("hedvig.multiplatform.library.android") id("hedvig.gradle.plugin") } @@ -9,58 +10,41 @@ hedvig { compose() } -android { - testOptions.unitTests.isReturnDefaultValues = true -} - -dependencies { - implementation(libs.androidx.compose.foundation) - implementation(libs.apollo.normalizedCache) - implementation(libs.apollo.runtime) - implementation(libs.arrow.core) - implementation(libs.arrow.fx) - 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.apolloNetworkCacheManager) - implementation(projects.apolloOctopusPublic) - implementation(projects.authCorePublic) - implementation(projects.composeUi) - implementation(projects.coreBuildConstants) - implementation(projects.coreCommonPublic) - implementation(projects.coreDatastorePublic) - implementation(projects.coreDemoMode) - implementation(projects.coreResources) - implementation(projects.coreUiData) - implementation(projects.dataContract) - implementation(projects.dataPayingMember) - implementation(projects.dataSettingsDatastorePublic) - implementation(projects.designSystemHedvig) - implementation(projects.featureFlagsPublic) - implementation(projects.foreverUi) - implementation(projects.languageCore) - implementation(projects.languageData) - implementation(projects.memberRemindersPublic) - implementation(projects.memberRemindersUi) - implementation(projects.moleculePublic) - implementation(projects.navigationCommon) - implementation(projects.navigationCompose) - implementation(projects.navigationComposeTyped) - implementation(projects.navigationCore) - implementation(projects.notificationPermission) - implementation(projects.pullrefresh) - implementation(projects.theme) - - testImplementation(libs.coroutines.test) - testImplementation(projects.coreCommonTest) - testImplementation(projects.coreDatastoreTest) - testImplementation(projects.dataSettingsDatastoreTest) - testImplementation(projects.featureFlagsTest) - testImplementation(projects.languageTest) - testImplementation(projects.memberRemindersTest) - testImplementation(projects.moleculeTest) +kotlin { + sourceSets { + commonMain.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) + } + androidMain.dependencies { + implementation(libs.bundles.kmpPreviewBugWorkaround) + } + } } diff --git a/app/feature/feature-payments/src/main/graphql/QueryDiscounts.graphql b/app/feature/feature-payments/src/commonMain/graphql/QueryDiscounts.graphql similarity index 100% rename from app/feature/feature-payments/src/main/graphql/QueryDiscounts.graphql rename to app/feature/feature-payments/src/commonMain/graphql/QueryDiscounts.graphql diff --git a/app/feature/feature-payments/src/main/graphql/QueryMemberPaymentDetails.graphql b/app/feature/feature-payments/src/commonMain/graphql/QueryMemberPaymentDetails.graphql similarity index 100% rename from app/feature/feature-payments/src/main/graphql/QueryMemberPaymentDetails.graphql rename to app/feature/feature-payments/src/commonMain/graphql/QueryMemberPaymentDetails.graphql diff --git a/app/feature/feature-payments/src/main/graphql/QueryPaymentsHistory.graphql b/app/feature/feature-payments/src/commonMain/graphql/QueryPaymentsHistory.graphql similarity index 100% rename from app/feature/feature-payments/src/main/graphql/QueryPaymentsHistory.graphql rename to app/feature/feature-payments/src/commonMain/graphql/QueryPaymentsHistory.graphql diff --git a/app/feature/feature-payments/src/main/graphql/QueryReferrals.graphql b/app/feature/feature-payments/src/commonMain/graphql/QueryReferrals.graphql similarity index 100% rename from app/feature/feature-payments/src/main/graphql/QueryReferrals.graphql rename to app/feature/feature-payments/src/commonMain/graphql/QueryReferrals.graphql diff --git a/app/feature/feature-payments/src/main/graphql/QueryShortPaymentsHistory.graphql b/app/feature/feature-payments/src/commonMain/graphql/QueryShortPaymentsHistory.graphql similarity index 100% rename from app/feature/feature-payments/src/main/graphql/QueryShortPaymentsHistory.graphql rename to app/feature/feature-payments/src/commonMain/graphql/QueryShortPaymentsHistory.graphql diff --git a/app/feature/feature-payments/src/commonMain/graphql/QueryShouldShowPayoutButton.graphql b/app/feature/feature-payments/src/commonMain/graphql/QueryShouldShowPayoutButton.graphql new file mode 100644 index 0000000000..0de72fbf20 --- /dev/null +++ b/app/feature/feature-payments/src/commonMain/graphql/QueryShouldShowPayoutButton.graphql @@ -0,0 +1,15 @@ +query ShouldShowPayoutButton { + currentMember { + paymentMethods { + defaultPayoutMethod { + provider + } + payoutMethods { + provider + } + availableMethods { + supportsPayout + } + } + } +} diff --git a/app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql b/app/feature/feature-payments/src/commonMain/graphql/QueryUpcomingPayment.graphql similarity index 100% rename from app/feature/feature-payments/src/main/graphql/QueryUpcomingPayment.graphql rename to app/feature/feature-payments/src/commonMain/graphql/QueryUpcomingPayment.graphql diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/PreviewData.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/PreviewData.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/PreviewData.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/PreviewData.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/Discount.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/Discount.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/Discount.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/Discount.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetChargeDetailsUseCase.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/GetChargeDetailsUseCase.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetChargeDetailsUseCase.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/GetChargeDetailsUseCase.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetDiscountsOverviewUseCase.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/GetDiscountsOverviewUseCase.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetDiscountsOverviewUseCase.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/GetDiscountsOverviewUseCase.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetDiscountsUseCase.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/GetDiscountsUseCase.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetDiscountsUseCase.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/GetDiscountsUseCase.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetMemberPaymentsDetailsUseCase.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/GetMemberPaymentsDetailsUseCase.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetMemberPaymentsDetailsUseCase.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/GetMemberPaymentsDetailsUseCase.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetPaymentsHistoryUseCase.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/GetPaymentsHistoryUseCase.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetPaymentsHistoryUseCase.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/GetPaymentsHistoryUseCase.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt similarity index 95% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt index 99fada4411..9f98e86fc5 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt +++ b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt @@ -3,13 +3,10 @@ package com.hedvig.android.feature.payments.data import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney import com.hedvig.android.feature.payments.data.Discount.DiscountStatus -import kotlin.String -import kotlin.time.Clock +import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone import kotlinx.datetime.daysUntil -import kotlinx.datetime.toJavaLocalDate -import kotlinx.datetime.todayIn +import kotlinx.datetime.plus import kotlinx.serialization.Serializable import octopus.PaymentHistoryWithDetailsQuery import octopus.ShortPaymentHistoryQuery @@ -74,7 +71,7 @@ internal data class MemberCharge( val isPreviouslyFailedCharge: Boolean, ) { val description: Description? = when { - fromDate.dayOfMonth == 1 && toDate.isLastDayOfMonth() -> { + fromDate.day == 1 && toDate.isLastDayOfMonth() -> { Description.FullPeriod } @@ -195,5 +192,5 @@ internal fun MemberChargeFragment.toFailedCharge(): MemberCharge.FailedCharge? { } fun LocalDate.isLastDayOfMonth(): Boolean { - return toJavaLocalDate().lengthOfMonth() == dayOfMonth + return plus(1, DateTimeUnit.DAY).day == 1 } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentConnection.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/PaymentConnection.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentConnection.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/PaymentConnection.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentOverview.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/PaymentOverview.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/PaymentOverview.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/data/PaymentOverview.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt similarity index 83% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt index 7272dbcefd..4471be10e5 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt +++ b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/di/PaymentsModule.kt @@ -15,6 +15,10 @@ import com.hedvig.android.feature.payments.data.GetPaymentsHistoryUseCase import com.hedvig.android.feature.payments.data.GetPaymentsHistoryUseCaseImpl 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 +import com.hedvig.android.feature.payments.overview.data.GetShouldShowPayoutUseCaseDemo +import com.hedvig.android.feature.payments.overview.data.GetShouldShowPayoutUseCaseImpl +import com.hedvig.android.feature.payments.overview.data.GetShouldShowPayoutUseCaseProvider import com.hedvig.android.feature.payments.overview.data.GetUpcomingPaymentUseCase import com.hedvig.android.feature.payments.overview.data.GetUpcomingPaymentUseCaseDemo import com.hedvig.android.feature.payments.overview.data.GetUpcomingPaymentUseCaseImpl @@ -67,6 +71,7 @@ val paymentsModule = module { viewModel { PaymentsViewModel( get(), + get(), ) } @@ -116,4 +121,19 @@ val paymentsModule = module { clock = get(), ) } + single { + GetShouldShowPayoutUseCaseProvider( + demoManager = get(), + demoImpl = get(), + prodImpl = get(), + ) + } + single { + GetShouldShowPayoutUseCaseImpl( + get(), + ) + } + single { + GetShouldShowPayoutUseCaseDemo() + } } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt similarity index 97% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt index 20fa3dd7a4..d0f5b8698c 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt +++ b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt @@ -32,6 +32,7 @@ fun NavGraphBuilder.paymentsGraph( languageService: LanguageService, hedvigBuildConstants: HedvigBuildConstants, navigateToConnectPayment: () -> Unit, + navigateToPayoutAccount: () -> Unit, onOpenChat: () -> Unit, ) { navgraph( @@ -48,6 +49,7 @@ fun NavGraphBuilder.paymentsGraph( onPaymentHistoryClicked = dropUnlessResumed { navController.navigate(PaymentsDestinations.History) }, + onPayoutAccountClicked = dropUnlessResumed { navigateToPayoutAccount() }, onChangeBankAccount = dropUnlessResumed { navigateToConnectPayment() }, onDiscountClicked = dropUnlessResumed { navController.navigate(PaymentsDestinations.Discounts) diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetForeverInformationUseCase.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/overview/data/GetForeverInformationUseCase.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetForeverInformationUseCase.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/overview/data/GetForeverInformationUseCase.kt diff --git a/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/overview/data/GetShouldShowPayoutUseCase.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/overview/data/GetShouldShowPayoutUseCase.kt new file mode 100644 index 0000000000..111a706c50 --- /dev/null +++ b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/overview/data/GetShouldShowPayoutUseCase.kt @@ -0,0 +1,41 @@ +package com.hedvig.android.feature.payments.overview.data + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.right +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 octopus.ShouldShowPayoutButtonQuery + +internal interface GetShouldShowPayoutUseCase { + suspend fun invoke(): Either +} + +/** + * We do not want to show the payout button at all when there is no payout method connected nor is there a possibility + * to add one in the member's current state + */ +internal class GetShouldShowPayoutUseCaseImpl( + private val apolloClient: ApolloClient, +) : GetShouldShowPayoutUseCase { + override suspend fun invoke(): Either = either { + val result = apolloClient + .query(ShouldShowPayoutButtonQuery()) + .fetchPolicy(FetchPolicy.NetworkFirst) + .safeExecute(::ErrorMessage) + .bind() + + val paymentMethods = result.currentMember.paymentMethods + paymentMethods.availableMethods.any { it.supportsPayout } || + paymentMethods.defaultPayoutMethod != null || + paymentMethods.payoutMethods.isNotEmpty() + } +} + +internal class GetShouldShowPayoutUseCaseDemo : GetShouldShowPayoutUseCase { + override suspend fun invoke(): Either = false.right() +} diff --git a/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/overview/data/GetShouldShowPayoutUseCaseProvider.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/overview/data/GetShouldShowPayoutUseCaseProvider.kt new file mode 100644 index 0000000000..1577822747 --- /dev/null +++ b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/overview/data/GetShouldShowPayoutUseCaseProvider.kt @@ -0,0 +1,10 @@ +package com.hedvig.android.feature.payments.overview.data + +import com.hedvig.android.core.demomode.DemoManager +import com.hedvig.android.core.demomode.ProdOrDemoProvider + +internal class GetShouldShowPayoutUseCaseProvider( + override val demoManager: DemoManager, + override val demoImpl: GetShouldShowPayoutUseCase, + override val prodImpl: GetShouldShowPayoutUseCase, +) : ProdOrDemoProvider 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/commonMain/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCaseProvider.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCaseProvider.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCaseProvider.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCaseProvider.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailExpandableCard.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailExpandableCard.kt similarity index 99% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailExpandableCard.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailExpandableCard.kt index 08b7e32e55..6cea80d2cc 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailExpandableCard.kt +++ b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailExpandableCard.kt @@ -66,7 +66,6 @@ import hedvig.resources.TALKBACK_EXPANDABLE_CLICK_LABEL_EXPAND import hedvig.resources.TALKBACK_EXPANDABLE_STATE_COLLAPSED import hedvig.resources.TALKBACK_EXPANDABLE_STATE_EXPANDED import kotlinx.datetime.LocalDate -import kotlinx.datetime.toJavaLocalDate import org.jetbrains.compose.resources.stringResource @Composable 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/commonMain/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsDestination.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsDestination.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsDestination.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsViewModel.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsViewModel.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsViewModel.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/details/PaymentDetailsViewModel.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/discounts/AddDiscountBottomSheetContent.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/discounts/AddDiscountBottomSheetContent.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/discounts/AddDiscountBottomSheetContent.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/discounts/AddDiscountBottomSheetContent.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountRow.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountRow.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountRow.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountRow.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountsDestination.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountsDestination.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountsDestination.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountsDestination.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountsPresenter.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountsPresenter.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountsPresenter.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountsPresenter.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountsViewModel.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountsViewModel.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountsViewModel.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/discounts/DiscountsViewModel.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/history/PaymentHistoryDestination.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/history/PaymentHistoryDestination.kt similarity index 99% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/history/PaymentHistoryDestination.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/history/PaymentHistoryDestination.kt index 5858b5acd7..5747eb73c7 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/history/PaymentHistoryDestination.kt +++ b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/history/PaymentHistoryDestination.kt @@ -40,7 +40,6 @@ import hedvig.resources.PAYMENTS_NO_HISTORY_DATA import hedvig.resources.PAYMENT_HISTORY_TITLE import hedvig.resources.Res import kotlinx.datetime.LocalDate -import kotlinx.datetime.toJavaLocalDate import org.jetbrains.compose.resources.stringResource @Composable diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/history/PaymentHistoryViewModel.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/history/PaymentHistoryViewModel.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/history/PaymentHistoryViewModel.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/history/PaymentHistoryViewModel.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsDestination.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsDestination.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsDestination.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsDestination.kt diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsViewModel.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsViewModel.kt similarity index 100% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsViewModel.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsViewModel.kt 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/commonMain/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt similarity index 95% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt index f630e86423..63ad6b0704 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt +++ b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt @@ -62,6 +62,7 @@ import com.hedvig.android.design.system.hedvig.icon.Card import com.hedvig.android.design.system.hedvig.icon.ChevronRight import com.hedvig.android.design.system.hedvig.icon.Clock import com.hedvig.android.design.system.hedvig.icon.HedvigIcons +import com.hedvig.android.design.system.hedvig.icon.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 @@ -111,6 +112,7 @@ internal fun PaymentsDestination( onPaymentClicked: (id: String?) -> Unit, onDiscountClicked: () -> Unit, onPaymentHistoryClicked: () -> Unit, + onPayoutAccountClicked: () -> Unit, onMemberPaymentDetailsClicked: () -> Unit, onChangeBankAccount: () -> Unit, ) { @@ -121,6 +123,7 @@ internal fun PaymentsDestination( onChangeBankAccount = onChangeBankAccount, onDiscountClicked = onDiscountClicked, onPaymentHistoryClicked = onPaymentHistoryClicked, + onPayoutAccountClicked = onPayoutAccountClicked, onRetry = { viewModel.emit(Retry) }, onPaymentDetailsClicked = onMemberPaymentDetailsClicked, ) @@ -133,6 +136,7 @@ private fun PaymentsScreen( onChangeBankAccount: () -> Unit, onDiscountClicked: () -> Unit, onPaymentHistoryClicked: () -> Unit, + onPayoutAccountClicked: () -> Unit, onPaymentDetailsClicked: () -> Unit, onRetry: () -> Unit, ) { @@ -194,6 +198,7 @@ private fun PaymentsScreen( onChangeBankAccount = onChangeBankAccount, onDiscountClicked = onDiscountClicked, onPaymentHistoryClicked = onPaymentHistoryClicked, + onPayoutAccountClicked = onPayoutAccountClicked, onPaymentDetailsClicked = onPaymentDetailsClicked, ) Spacer(Modifier.height(16.dp)) @@ -218,6 +223,7 @@ private fun PaymentsContent( onChangeBankAccount: () -> Unit, onDiscountClicked: () -> Unit, onPaymentHistoryClicked: () -> Unit, + onPayoutAccountClicked: () -> Unit, onPaymentDetailsClicked: () -> Unit, modifier: Modifier = Modifier, ) { @@ -279,7 +285,9 @@ private fun PaymentsContent( uiState, onDiscountClicked = onDiscountClicked, onPaymentHistoryClicked = onPaymentHistoryClicked, + onPayoutAccountClicked = onPayoutAccountClicked, onPaymentDetailsClicked = onPaymentDetailsClicked, + showPayoutButton = (uiState as? Content)?.showPayoutButton == true, ) if (uiState is Content) { when (uiState.connectedPaymentInfo) { @@ -375,7 +383,9 @@ private fun PaymentsListItems( uiState: PaymentsUiState, onDiscountClicked: () -> Unit, onPaymentHistoryClicked: () -> Unit, + onPayoutAccountClicked: () -> Unit, onPaymentDetailsClicked: () -> Unit, + showPayoutButton: Boolean, ) { val listItemsSideSpacingModifier = Modifier .padding(horizontal = 16.dp) @@ -397,6 +407,24 @@ private fun PaymentsListItems( .padding(vertical = 16.dp) .fillMaxWidth(), ) + if (showPayoutButton) { + HorizontalDivider(modifier = listItemsSideSpacingModifier) + PaymentsListItem( + text = "Payout", + icon = { + Icon( + imageVector = HedvigIcons.PaymentOutline, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + }, + modifier = Modifier + .clickable(onClick = onPayoutAccountClicked) + .then(listItemsSideSpacingModifier) + .padding(vertical = 16.dp) + .fillMaxWidth(), + ) + } HorizontalDivider(modifier = listItemsSideSpacingModifier) PaymentsListItem( text = stringResource(Res.string.PAYMENTS_PAYMENT_HISTORY_BUTTON_LABEL), @@ -592,6 +620,7 @@ private fun PreviewPaymentScreen( {}, {}, {}, + {}, ) } } @@ -611,6 +640,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< "Card", "****1234", ), + showPayoutButton = false, ), ) add( @@ -627,6 +657,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< "Card", "****1234", ), + showPayoutButton = false, ), ) add( @@ -643,6 +674,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< "Card", "****1234", ), + showPayoutButton = false, ), ) add( @@ -662,6 +694,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< "Card", "****1234", ), + showPayoutButton = false, ), ) add( @@ -675,6 +708,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< upcomingPaymentInfo = NoInfo, ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.Pending, + showPayoutButton = false, ), ) add( @@ -690,6 +724,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( null, ), + showPayoutButton = false, ), ) add( @@ -705,6 +740,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( null, ), + showPayoutButton = false, ), ) add( @@ -723,6 +759,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( dueDateToConnect = System.now().plus(30.days).toLocalDateTime(TimeZone.UTC).date, ), + showPayoutButton = false, ), ) add( @@ -741,6 +778,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( System.now().plus(30.days).toLocalDateTime(TimeZone.UTC).date, ), + showPayoutButton = false, ), ) }, diff --git a/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt new file mode 100644 index 0000000000..21423a16dc --- /dev/null +++ b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt @@ -0,0 +1,183 @@ +package com.hedvig.android.feature.payments.ui.payments + +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 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.MemberCharge +import com.hedvig.android.feature.payments.data.PaymentConnection +import com.hedvig.android.feature.payments.data.PaymentConnection.Active +import com.hedvig.android.feature.payments.data.PaymentConnection.NeedsSetup +import com.hedvig.android.feature.payments.data.PaymentConnection.Pending +import com.hedvig.android.feature.payments.data.PaymentConnection.Unknown +import com.hedvig.android.feature.payments.data.PaymentOverview +import com.hedvig.android.feature.payments.data.PaymentOverview.OngoingCharge +import com.hedvig.android.feature.payments.overview.data.GetShouldShowPayoutUseCase +import com.hedvig.android.feature.payments.overview.data.GetUpcomingPaymentUseCase +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 kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.delay +import kotlinx.datetime.LocalDate + +internal class PaymentsPresenter( + private val getUpcomingPaymentUseCase: Provider, + getShouldShowPayoutUseCase: Provider, +) : MoleculePresenter { + private val shouldShowPayoutPresenter = ShouldShowPayoutPresenter(getShouldShowPayoutUseCase) + @Composable + override fun MoleculePresenterScope.present(lastState: PaymentsUiState): PaymentsUiState { + var loadIteration by remember { mutableIntStateOf(0) } + var paymentOverviewResult: Either? by remember { mutableStateOf(null) } + + CollectEvents { event -> + when (event) { + PaymentsEvent.Retry -> loadIteration++ + } + } + + LaunchedEffect(loadIteration) { + paymentOverviewResult = null + paymentOverviewResult = getUpcomingPaymentUseCase.provide().invoke() + } + + val shouldShowPayout = shouldShowPayoutPresenter.present(loadIteration) + + val currentPaymentResult = paymentOverviewResult ?: return PaymentsUiState.Loading + + return currentPaymentResult.fold( + ifLeft = { PaymentsUiState.Error }, + ifRight = { paymentOverview -> + PaymentsUiState.Content( + isRetrying = false, + upcomingPayment = paymentOverview.memberChargeShortInfo?.let { memberCharge -> + PaymentsUiState.Content.UpcomingPayment.Content( + netAmount = memberCharge.netAmount, + dueDate = memberCharge.dueDate, + id = memberCharge.id, + ) + } ?: PaymentsUiState.Content.UpcomingPayment.NoUpcomingPayment, + upcomingPaymentInfo = run { + val memberCharge = paymentOverview.memberChargeShortInfo + if (memberCharge?.status == MemberCharge.MemberChargeStatus.PENDING) { + return@run PaymentsUiState.Content.UpcomingPaymentInfo.InProgress + } + memberCharge?.failedCharge?.let { failedCharge -> + return@run PaymentsUiState.Content.UpcomingPaymentInfo.PaymentFailed( + failedPaymentStartDate = failedCharge.fromDate, + failedPaymentEndDate = failedCharge.toDate, + ) + } + PaymentsUiState.Content.UpcomingPaymentInfo.NoInfo + }, + ongoingCharges = paymentOverview.ongoingCharges, + connectedPaymentInfo = paymentOverview.paymentConnection.toConnectedPaymentInfo(), + showPayoutButton = shouldShowPayout, + ) + }, + ) + } +} + +private class ShouldShowPayoutPresenter( + private val getShouldShowPayoutUseCase: Provider, +) { + @Composable + fun present(loadIteration: Int): Boolean { + var shouldShowPayout by remember { mutableStateOf(false) } + LaunchedEffect(loadIteration) { + shouldShowPayout = false + for (attempt in 0..2) { + delay(attempt.seconds) + getShouldShowPayoutUseCase.provide().invoke().fold( + ifLeft = {}, + ifRight = { result -> + shouldShowPayout = result + return@LaunchedEffect + }, + ) + } + } + return shouldShowPayout + } +} + +private fun PaymentConnection.toConnectedPaymentInfo(): ConnectedPaymentInfo { + return when (this) { + is Active -> ConnectedPaymentInfo.Active( + displayName = displayName, + maskedAccountNumber = displayValue, + ) + + Pending -> ConnectedPaymentInfo.Pending + + is NeedsSetup -> ConnectedPaymentInfo.NeedsSetup( + dueDateToConnect = terminationDateIfNotConnected, + ) + + Unknown -> ConnectedPaymentInfo.Unknown + } +} + +internal sealed interface PaymentsEvent { + data object Retry : PaymentsEvent +} + +internal sealed interface PaymentsUiState { + data object Error : PaymentsUiState + + data object Loading : PaymentsUiState + + data class Content( + val isRetrying: Boolean, + val upcomingPayment: UpcomingPayment, + val upcomingPaymentInfo: UpcomingPaymentInfo, + val ongoingCharges: List, + val connectedPaymentInfo: ConnectedPaymentInfo, + val showPayoutButton: Boolean, + ) : PaymentsUiState { + sealed interface UpcomingPayment { + data object NoUpcomingPayment : UpcomingPayment + + data class Content( + val netAmount: UiMoney, + val dueDate: LocalDate, + val id: String?, + ) : UpcomingPayment + } + + sealed interface UpcomingPaymentInfo { + data object NoInfo : UpcomingPaymentInfo + + data object InProgress : UpcomingPaymentInfo + + data class PaymentFailed( + val failedPaymentStartDate: LocalDate, + val failedPaymentEndDate: LocalDate, + ) : UpcomingPaymentInfo + } + + sealed interface ConnectedPaymentInfo { + object Unknown : ConnectedPaymentInfo + + data class NeedsSetup( + val dueDateToConnect: LocalDate?, + ) : ConnectedPaymentInfo + + data object Pending : ConnectedPaymentInfo + + data class Active( + val displayName: String?, + val maskedAccountNumber: String?, + ) : ConnectedPaymentInfo + } + } +} diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsViewModel.kt b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsViewModel.kt similarity index 71% rename from app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsViewModel.kt rename to app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsViewModel.kt index 616b9004ca..3486475610 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsViewModel.kt +++ b/app/feature/feature-payments/src/commonMain/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsViewModel.kt @@ -1,14 +1,17 @@ package com.hedvig.android.feature.payments.ui.payments import com.hedvig.android.core.demomode.Provider +import com.hedvig.android.feature.payments.overview.data.GetShouldShowPayoutUseCase import com.hedvig.android.feature.payments.overview.data.GetUpcomingPaymentUseCase import com.hedvig.android.molecule.public.MoleculeViewModel internal class PaymentsViewModel( getUpcomingPaymentUseCase: Provider, + getShouldShowPayoutUseCase: Provider, ) : MoleculeViewModel( PaymentsUiState.Loading, PaymentsPresenter( getUpcomingPaymentUseCase = getUpcomingPaymentUseCase, + getShouldShowPayoutUseCase = getShouldShowPayoutUseCase, ), ) diff --git a/app/feature/feature-payments/src/main/AndroidManifest.xml b/app/feature/feature-payments/src/main/AndroidManifest.xml deleted file mode 100644 index 568741e54f..0000000000 --- a/app/feature/feature-payments/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file 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 deleted file mode 100644 index 18c59e81aa..0000000000 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.hedvig.android.feature.payments.ui.payments - -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.demomode.Provider -import com.hedvig.android.core.uidata.UiMoney -import com.hedvig.android.feature.payments.data.MemberCharge -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 -import com.hedvig.android.feature.payments.data.PaymentConnection.Unknown -import com.hedvig.android.feature.payments.data.PaymentOverview.OngoingCharge -import com.hedvig.android.feature.payments.overview.data.GetUpcomingPaymentUseCase -import com.hedvig.android.molecule.public.MoleculePresenter -import com.hedvig.android.molecule.public.MoleculePresenterScope -import kotlinx.datetime.LocalDate - -internal class PaymentsPresenter( - private val getUpcomingPaymentUseCase: Provider, -) : MoleculePresenter { - @Composable - override fun MoleculePresenterScope.present(lastState: PaymentsUiState): PaymentsUiState { - var paymentsUiState: PaymentsUiState by remember { mutableStateOf(lastState) } - var loadIteration by remember { mutableIntStateOf(0) } - - CollectEvents { event -> - when (event) { - PaymentsEvent.Retry -> loadIteration++ - } - } - - LaunchedEffect(loadIteration) { - val currentPaymentUiState = paymentsUiState - paymentsUiState = when (currentPaymentUiState) { - is PaymentsUiState.Content -> { - currentPaymentUiState.copy(isRetrying = true) - } - - else -> { - PaymentsUiState.Loading - } - } - getUpcomingPaymentUseCase.provide().invoke().fold( - ifLeft = { - paymentsUiState = PaymentsUiState.Error - }, - ifRight = { paymentOverview -> - paymentsUiState = PaymentsUiState.Content( - isRetrying = false, - upcomingPayment = paymentOverview.memberChargeShortInfo?.let { memberCharge -> - PaymentsUiState.Content.UpcomingPayment.Content( - netAmount = memberCharge.netAmount, - dueDate = memberCharge.dueDate, - id = memberCharge.id, - ) - } ?: PaymentsUiState.Content.UpcomingPayment.NoUpcomingPayment, - upcomingPaymentInfo = run { - val memberCharge = paymentOverview.memberChargeShortInfo - if (memberCharge?.status == MemberCharge.MemberChargeStatus.PENDING) { - return@run PaymentsUiState.Content.UpcomingPaymentInfo.InProgress - } - memberCharge?.failedCharge?.let { failedCharge -> - return@run PaymentsUiState.Content.UpcomingPaymentInfo.PaymentFailed( - failedPaymentStartDate = failedCharge.fromDate, - failedPaymentEndDate = failedCharge.toDate, - ) - } - PaymentsUiState.Content.UpcomingPaymentInfo.NoInfo - }, - ongoingCharges = paymentOverview.ongoingCharges, - connectedPaymentInfo = when (val paymentConnection = paymentOverview.paymentConnection) { - is Active -> { - PaymentsUiState.Content.ConnectedPaymentInfo.Active( - displayName = paymentConnection.displayName, - maskedAccountNumber = paymentConnection.displayValue, - ) - } - - Pending -> { - PaymentsUiState.Content.ConnectedPaymentInfo.Pending - } - - is NeedsSetup -> { - PaymentsUiState.Content.ConnectedPaymentInfo.NeedsSetup( - dueDateToConnect = paymentConnection.terminationDateIfNotConnected, - ) - } - - Unknown -> { - PaymentsUiState.Content.ConnectedPaymentInfo.Unknown - } - }, - ) - }, - ) - } - return paymentsUiState - } -} - -internal sealed interface PaymentsEvent { - data object Retry : PaymentsEvent -} - -internal sealed interface PaymentsUiState { - data object Error : PaymentsUiState - - data object Loading : PaymentsUiState - - data class Content( - val isRetrying: Boolean, - val upcomingPayment: UpcomingPayment, - val upcomingPaymentInfo: UpcomingPaymentInfo, - val ongoingCharges: List, - val connectedPaymentInfo: ConnectedPaymentInfo, - ) : PaymentsUiState { - sealed interface UpcomingPayment { - data object NoUpcomingPayment : UpcomingPayment - - data class Content( - val netAmount: UiMoney, - val dueDate: LocalDate, - val id: String?, - ) : UpcomingPayment - } - - sealed interface UpcomingPaymentInfo { - data object NoInfo : UpcomingPaymentInfo - - data object InProgress : UpcomingPaymentInfo - - data class PaymentFailed( - val failedPaymentStartDate: LocalDate, - val failedPaymentEndDate: LocalDate, - ) : UpcomingPaymentInfo - } - - sealed interface ConnectedPaymentInfo { - object Unknown : ConnectedPaymentInfo - - data class NeedsSetup( - val dueDateToConnect: LocalDate?, - ) : ConnectedPaymentInfo - - data object Pending : ConnectedPaymentInfo - - data class Active( - val displayName: String?, - val maskedAccountNumber: String?, - ) : ConnectedPaymentInfo - } - } -} diff --git a/app/feature/feature-payout-account/build.gradle.kts b/app/feature/feature-payout-account/build.gradle.kts new file mode 100644 index 0000000000..73a55cf270 --- /dev/null +++ b/app/feature/feature-payout-account/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + id("hedvig.android.library") + id("hedvig.gradle.plugin") +} + +hedvig { + apollo("octopus") + serialization() + compose() +} + +dependencies { + implementation(libs.apollo.normalizedCache) + implementation(libs.apollo.runtime) + implementation(libs.arrow.core) + implementation(projects.apolloCore) + implementation(projects.apolloNetworkCacheManager) + implementation(projects.apolloOctopusPublic) + implementation(projects.coreBuildConstants) + 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(projects.composeUi) + implementation(projects.coreCommonPublic) + implementation(projects.coreResources) + implementation(projects.designSystemHedvig) + implementation(projects.moleculePublic) + implementation(projects.navigationCommon) + implementation(projects.navigationCompose) + implementation(projects.navigationComposeTyped) +} diff --git a/app/feature/feature-payout-account/src/main/graphql/GetPayoutMethods.graphql b/app/feature/feature-payout-account/src/main/graphql/GetPayoutMethods.graphql new file mode 100644 index 0000000000..98c876de37 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/graphql/GetPayoutMethods.graphql @@ -0,0 +1,22 @@ +query GetPayoutMethods { + currentMember { + paymentMethods { + defaultPayoutMethod { + provider + details { + ... on PaymentMethodBankAccountDetails { + account + bank + } + ... on PaymentMethodSwishDetails { + phoneNumber + } + } + } + availableMethods { + provider + supportsPayout + } + } + } +} diff --git a/app/feature/feature-payout-account/src/main/graphql/SetupNordeaPayout.graphql b/app/feature/feature-payout-account/src/main/graphql/SetupNordeaPayout.graphql new file mode 100644 index 0000000000..4de67f6f87 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/graphql/SetupNordeaPayout.graphql @@ -0,0 +1,10 @@ +mutation SetupNordeaPayout($clearingNumber: String!, $accountNumber: String!) { + paymentMethodSetupNordeaPayout( + input: { clearingNumber: $clearingNumber, accountNumber: $accountNumber } + ) { + status + error { + message + } + } +} diff --git a/app/feature/feature-payout-account/src/main/graphql/SetupSwishPayout.graphql b/app/feature/feature-payout-account/src/main/graphql/SetupSwishPayout.graphql new file mode 100644 index 0000000000..347b1a44b9 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/graphql/SetupSwishPayout.graphql @@ -0,0 +1,8 @@ +mutation SetupSwishPayout($phoneNumber: String!) { + paymentMethodSetupSwishPayout( + input: { phoneNumber: $phoneNumber } + ) { + status + error { message } + } +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/BankNameLookup.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/BankNameLookup.kt new file mode 100644 index 0000000000..2117996ec2 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/BankNameLookup.kt @@ -0,0 +1,78 @@ +package com.hedvig.android.feature.payoutaccount.data + +/** + * Todo bring this information from backend possibly + * https://www.bankinfrastruktur.se/media/kelmctkm/1906_clearingnummer-institut-221212_-nummerordning.pdf + */ +internal fun bankNameForClearingNumber(clearingNumber: String): String? { + val number = clearingNumber.toIntOrNull() ?: return null + return when (number) { + in 1000..1099 -> "Sveriges Riksbank" + in 1100..1199 -> "Nordea" + in 1200..1399 -> "Danske Bank" + in 1400..2099 -> "Nordea" + in 2300..2399 -> "Ålandsbanken" + in 2400..2499 -> "Danske Bank" + in 3000..3399 -> "Nordea" + in 3400..3409 -> "Länsförsäkringar Bank" + in 3410..4999 -> "Nordea" + in 5000..5999 -> "SEB" + in 6000..6999 -> "Handelsbanken" + in 7000..8999 -> "Swedbank" + in 9020..9029 -> "Länsförsäkringar Bank" + in 9040..9049 -> "Citibank" + in 9060..9069 -> "Länsförsäkringar Bank" + in 9070..9079 -> "Multitude Bank" + in 9080..9089 -> "Crédit Agricole Corporate" + in 9100..9109 -> "Nordnet Bank" + in 9120..9124 -> "SEB" + in 9130..9149 -> "SEB" + in 9150..9169 -> "Skandiabanken" + in 9170..9179 -> "IKANO Banken" + in 9180..9189 -> "Danske Bank" + in 9190..9199 -> "DNB Bank" + in 9230..9239 -> "Marginalen Bank" + in 9250..9259 -> "SBAB Bank" + in 9260..9269 -> "DNB Bank" + in 9270..9279 -> "ICA Banken" + in 9280..9289 -> "Resurs Bank" + in 9300..9349 -> "Swedbank" + in 9380..9389 -> "Pareto Securities" + in 9390..9399 -> "Landshypotek" + in 9400..9449 -> "Forex Bank" + in 9460..9469 -> "Santander Consumer Bank" + in 9470..9479 -> "BNP Paribas" + in 9490..9499 -> "Brite" + in 9500..9549 -> "Nordea" + in 9550..9569 -> "Avanza Bank" + in 9570..9579 -> "Sparbanken Syd" + in 9580..9589 -> "AION Bank" + in 9590..9599 -> "Erik Penser Bank" + in 9600..9609 -> "Banking Circle" + in 9610..9619 -> "Volvofinans Bank" + in 9620..9629 -> "Bank of China" + in 9630..9639 -> "Lån & Spar Bank" + in 9640..9649 -> "Nordax Bank" + in 9650..9659 -> "MedMera Bank" + in 9660..9669 -> "Svea Bank" + in 9670..9679 -> "JAK Medlemsbank" + in 9680..9689 -> "Bluestep Finans" + in 9690..9699 -> "Folkia" + in 9700..9709 -> "Ekobanken" + in 9710..9719 -> "Lunar Bank" + in 9750..9759 -> "Northmill Bank" + in 9770..9779 -> "Intergiro" + in 9780..9789 -> "Klarna Bank" + in 9860..9869 -> "Privatgirot" + in 9870..9879 -> "Nasdaq OMX" + in 9880..9899 -> "Riksgälden" + 9951 -> "Teller Branch Norway" + 9952 -> "Bankernas Automatbolag" + 9953 -> "Teller Branch Sweden" + 9954 -> "Kortaccept Nordic" + 9955 -> "Kommuninvest" + 9956 -> "VP Securities" + in 9960..9969 -> "Nordea" + else -> null + } +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/GetPayoutAccountUseCase.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/GetPayoutAccountUseCase.kt new file mode 100644 index 0000000000..5e79eb0cbd --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/GetPayoutAccountUseCase.kt @@ -0,0 +1,76 @@ +package com.hedvig.android.feature.payoutaccount.data + +import arrow.core.Either +import arrow.core.raise.either +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.cache.normalized.FetchPolicy +import com.apollographql.apollo.cache.normalized.fetchPolicy +import com.hedvig.android.apollo.ErrorMessage +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import octopus.GetPayoutMethodsQuery +import octopus.GetPayoutMethodsQuery.Data.CurrentMember.PaymentMethods.DefaultPayoutMethod.Details.Companion.asPaymentMethodBankAccountDetails +import octopus.GetPayoutMethodsQuery.Data.CurrentMember.PaymentMethods.DefaultPayoutMethod.Details.Companion.asPaymentMethodSwishDetails +import octopus.type.MemberPaymentProvider + +internal data class PayoutAccountData( + val currentMethod: PayoutAccount?, + val availablePayoutMethods: List, +) + +internal interface GetPayoutAccountUseCase { + suspend fun invoke(): Either +} + +internal class GetPayoutAccountUseCaseImpl( + private val apolloClient: ApolloClient, +) : GetPayoutAccountUseCase { + override suspend fun invoke(): Either = either { + val result = apolloClient + .query(GetPayoutMethodsQuery()) + .fetchPolicy(FetchPolicy.NetworkOnly) + .safeExecute(::ErrorMessage) + .bind() + + val paymentMethods = result.currentMember.paymentMethods + val currentMethod = paymentMethods.defaultPayoutMethod?.let { method -> + when (method.provider) { + MemberPaymentProvider.TRUSTLY -> PayoutAccount.Trustly + MemberPaymentProvider.SWISH -> { + val swishDetails = method.details.asPaymentMethodSwishDetails() + if (swishDetails != null) { + PayoutAccount.SwishPayout(phoneNumber = swishDetails.phoneNumber) + } else { + null + } + } + MemberPaymentProvider.NORDEA -> { + val bankAccountDetails = method.details.asPaymentMethodBankAccountDetails() + if (bankAccountDetails != null) { + val account = bankAccountDetails.account + val dashIndex = account.indexOf('-') + val clearingNumber = if (dashIndex >= 0) account.substring(0, dashIndex) else account + val accountNumber = if (dashIndex >= 0) account.substring(dashIndex + 1) else "" + PayoutAccount.BankAccount( + clearingNumber = clearingNumber, + accountNumber = accountNumber, + bankName = bankAccountDetails.bank, + ) + } else { + null + } + } + else -> null + } + } + + val availablePayoutMethods = paymentMethods.availableMethods + .filter { it.supportsPayout } + .map { it.provider } + + PayoutAccountData( + currentMethod = currentMethod, + availablePayoutMethods = availablePayoutMethods, + ) + } +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/PayoutAccount.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/PayoutAccount.kt new file mode 100644 index 0000000000..228fb4e454 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/PayoutAccount.kt @@ -0,0 +1,13 @@ +package com.hedvig.android.feature.payoutaccount.data + +internal sealed interface PayoutAccount { + data object Trustly : PayoutAccount + + data class SwishPayout(val phoneNumber: String) : PayoutAccount + + data class BankAccount( + val clearingNumber: String, + val accountNumber: String, + val bankName: String?, + ) : PayoutAccount +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/SetupNordeaPayoutUseCase.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/SetupNordeaPayoutUseCase.kt new file mode 100644 index 0000000000..8369ed530f --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/SetupNordeaPayoutUseCase.kt @@ -0,0 +1,39 @@ +package com.hedvig.android.feature.payoutaccount.data + +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.either +import arrow.core.right +import com.apollographql.apollo.ApolloClient +import com.hedvig.android.apollo.ErrorMessage +import com.hedvig.android.apollo.NetworkCacheManager +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import octopus.SetupNordeaPayoutMutation +import octopus.type.PaymentMethodSetupStatus + +internal interface SetupNordeaPayoutUseCase { + suspend fun invoke(clearingNumber: String, accountNumber: String): Either +} + +internal class SetupNordeaPayoutUseCaseImpl( + private val apolloClient: ApolloClient, + private val networkCacheManager: NetworkCacheManager, +) : SetupNordeaPayoutUseCase { + override suspend fun invoke(clearingNumber: String, accountNumber: String): Either = either { + val result = apolloClient + .mutation(SetupNordeaPayoutMutation(clearingNumber = clearingNumber, accountNumber = accountNumber)) + .safeExecute(::ErrorMessage) + .bind() + + val output = result.paymentMethodSetupNordeaPayout + when (output.status) { + PaymentMethodSetupStatus.FAILED -> { + raise(ErrorMessage(output.error?.message ?: "Failed to set up payout method")) + } + else -> { + networkCacheManager.clearCache() + } + } + } +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/SetupSwishPayoutUseCase.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/SetupSwishPayoutUseCase.kt new file mode 100644 index 0000000000..84b11df5e1 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/SetupSwishPayoutUseCase.kt @@ -0,0 +1,33 @@ +package com.hedvig.android.feature.payoutaccount.data + +import arrow.core.Either +import arrow.core.raise.either +import com.apollographql.apollo.ApolloClient +import com.hedvig.android.apollo.ErrorMessage +import com.hedvig.android.apollo.NetworkCacheManager +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import octopus.SetupSwishPayoutMutation +import octopus.type.PaymentMethodSetupStatus + +internal class SetupSwishPayoutUseCase( + private val apolloClient: ApolloClient, + private val networkCacheManager: NetworkCacheManager, +) { + suspend fun invoke(phoneNumber: String): Either = either { + val result = apolloClient + .mutation(SetupSwishPayoutMutation(phoneNumber = phoneNumber)) + .safeExecute(::ErrorMessage) + .bind() + + val output = result.paymentMethodSetupSwishPayout + when (output.status) { + PaymentMethodSetupStatus.FAILED -> { + raise(ErrorMessage(output.error?.message ?: "Failed to set up Swish payout")) + } + else -> { + networkCacheManager.clearCache() + } + } + } +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/di/PayoutAccountModule.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/di/PayoutAccountModule.kt new file mode 100644 index 0000000000..3d21498662 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/di/PayoutAccountModule.kt @@ -0,0 +1,27 @@ +package com.hedvig.android.feature.payoutaccount.di + +import com.apollographql.apollo.ApolloClient +import com.hedvig.android.apollo.NetworkCacheManager +import com.hedvig.android.feature.payoutaccount.data.GetPayoutAccountUseCase +import com.hedvig.android.feature.payoutaccount.data.GetPayoutAccountUseCaseImpl +import com.hedvig.android.feature.payoutaccount.data.SetupNordeaPayoutUseCase +import com.hedvig.android.feature.payoutaccount.data.SetupNordeaPayoutUseCaseImpl +import com.hedvig.android.feature.payoutaccount.data.SetupSwishPayoutUseCase +import com.hedvig.android.feature.payoutaccount.ui.editbankaccount.EditBankAccountViewModel +import com.hedvig.android.feature.payoutaccount.ui.overview.PayoutAccountOverviewViewModel +import com.hedvig.android.feature.payoutaccount.ui.setupswish.SetupSwishPayoutViewModel +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module + +val payoutAccountModule = module { + single { GetPayoutAccountUseCaseImpl(get()) } + single { + SetupNordeaPayoutUseCaseImpl(get(), get()) + } + single { + SetupSwishPayoutUseCase(get(), get()) + } + viewModel { PayoutAccountOverviewViewModel(get()) } + viewModel { EditBankAccountViewModel(get()) } + viewModel { SetupSwishPayoutViewModel(get()) } +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/navigation/PayoutAccountDestination.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/navigation/PayoutAccountDestination.kt new file mode 100644 index 0000000000..aabd195ec5 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/navigation/PayoutAccountDestination.kt @@ -0,0 +1,25 @@ +package com.hedvig.android.feature.payoutaccount.navigation + +import com.hedvig.android.navigation.common.Destination +import kotlinx.serialization.Serializable + +sealed interface PayoutAccountDestination { + @Serializable + data object Graph : PayoutAccountDestination, Destination +} + +internal sealed interface PayoutAccountDestinations { + @Serializable + data object Overview : PayoutAccountDestinations, Destination + + @Serializable + data class SelectPayoutMethod( + val availableProviders: List, + ) : PayoutAccountDestinations, Destination + + @Serializable + data object EditBankAccount : PayoutAccountDestinations, Destination + + @Serializable + data object SetupSwishPayout : PayoutAccountDestinations, Destination +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/navigation/PayoutAccountGraph.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/navigation/PayoutAccountGraph.kt new file mode 100644 index 0000000000..4a330fd1ad --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/navigation/PayoutAccountGraph.kt @@ -0,0 +1,72 @@ +package com.hedvig.android.feature.payoutaccount.navigation + +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import com.hedvig.android.feature.payoutaccount.ui.editbankaccount.EditBankAccountDestination +import com.hedvig.android.feature.payoutaccount.ui.editbankaccount.EditBankAccountViewModel +import com.hedvig.android.feature.payoutaccount.ui.overview.PayoutAccountOverviewDestination +import com.hedvig.android.feature.payoutaccount.ui.overview.PayoutAccountOverviewUiState +import com.hedvig.android.feature.payoutaccount.ui.overview.PayoutAccountOverviewViewModel +import com.hedvig.android.feature.payoutaccount.ui.selectmethod.SelectPayoutMethodDestination +import com.hedvig.android.feature.payoutaccount.ui.setupswish.SetupSwishPayoutDestination +import com.hedvig.android.feature.payoutaccount.ui.setupswish.SetupSwishPayoutViewModel +import com.hedvig.android.navigation.compose.navdestination +import com.hedvig.android.navigation.compose.navgraph +import octopus.type.MemberPaymentProvider +import org.koin.compose.viewmodel.koinViewModel + +fun NavGraphBuilder.payoutAccountGraph( + navController: NavController, + navigateToTrustlyPayout: () -> Unit, + navigateUp: () -> Unit, +) { + navgraph( + startDestination = PayoutAccountDestinations.Overview::class, + ) { + navdestination { + val viewModel: PayoutAccountOverviewViewModel = koinViewModel() + PayoutAccountOverviewDestination( + viewModel = viewModel, + onConnectPayoutMethodClicked = dropUnlessResumed { + val content = viewModel.uiState.value as? PayoutAccountOverviewUiState.Content + navController.navigate( + PayoutAccountDestinations.SelectPayoutMethod( + availableProviders = content?.availablePayoutMethods?.map { it.rawValue } ?: emptyList(), + ), + ) + }, + onEditBankAccountClicked = dropUnlessResumed { + navController.navigate(PayoutAccountDestinations.EditBankAccount) + }, + navigateUp = navigateUp, + ) + } + + navdestination { + SelectPayoutMethodDestination( + availableProviders = this.availableProviders.map { MemberPaymentProvider.safeValueOf(it) }, + onTrustlySelected = dropUnlessResumed { navigateToTrustlyPayout() }, + onNordeaSelected = dropUnlessResumed { navController.navigate(PayoutAccountDestinations.EditBankAccount) }, + onSwishSelected = dropUnlessResumed { navController.navigate(PayoutAccountDestinations.SetupSwishPayout) }, + navigateUp = navController::navigateUp, + ) + } + + navdestination { + val viewModel: EditBankAccountViewModel = koinViewModel() + EditBankAccountDestination( + viewModel = viewModel, + navigateUp = navController::navigateUp, + ) + } + + navdestination { + val viewModel: SetupSwishPayoutViewModel = koinViewModel() + SetupSwishPayoutDestination( + viewModel = viewModel, + navigateUp = navController::navigateUp, + ) + } + } +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/editbankaccount/EditBankAccountDestination.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/editbankaccount/EditBankAccountDestination.kt new file mode 100644 index 0000000000..012575d59f --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/editbankaccount/EditBankAccountDestination.kt @@ -0,0 +1,111 @@ +package com.hedvig.android.feature.payoutaccount.ui.editbankaccount + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigNotificationCard +import com.hedvig.android.design.system.hedvig.HedvigScaffold +import com.hedvig.android.design.system.hedvig.HedvigText +import com.hedvig.android.design.system.hedvig.HedvigTextField +import com.hedvig.android.design.system.hedvig.HedvigTextFieldDefaults +import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority + +@Composable +internal fun EditBankAccountDestination( + viewModel: EditBankAccountViewModel, + navigateUp: () -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + EditBankAccountScreen( + uiState = uiState, + onSave = { viewModel.emit(EditBankAccountEvent.Save) }, + navigateUp = navigateUp, + ) +} + +@Composable +private fun EditBankAccountScreen( + uiState: EditBankAccountUiState, + onSave: () -> Unit, + navigateUp: () -> Unit, +) { + LaunchedEffect(uiState.navigateBack) { + if (uiState.navigateBack) navigateUp() + } + + HedvigScaffold( + topAppBarText = "Bank account", + navigateUp = navigateUp, + modifier = Modifier.fillMaxSize(), + ) { + Spacer(Modifier.weight(1f)) + Column(Modifier.padding(horizontal = 16.dp)) { + HedvigTextField( + state = uiState.clearingNumberState, + labelText = "Clearing", + textFieldSize = HedvigTextFieldDefaults.TextFieldSize.Large, + inputTransformation = uiState.clearingInputTransformation, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + trailingContent = uiState.bankName?.let { + { HedvigText(text = it) } + }, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(4.dp)) + HedvigTextField( + state = uiState.accountNumberState, + labelText = "Account number", + textFieldSize = HedvigTextFieldDefaults.TextFieldSize.Large, + inputTransformation = uiState.accountNumberInputTransformation, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + modifier = Modifier.fillMaxWidth(), + ) + } + AnimatedVisibility( + visible = uiState.errorMessage != null, + enter = expandVertically(), + exit = shrinkVertically(), + ) { + HedvigNotificationCard( + message = uiState.errorMessage ?: "", + priority = NotificationPriority.Attention, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 4.dp) + .fillMaxWidth(), + ) + } + Spacer(Modifier.height(16.dp)) + HedvigButton( + text = "Save", + onClick = onSave, + enabled = !uiState.isLoading && + uiState.clearingNumberState.text.isNotBlank() && + uiState.accountNumberState.text.isNotBlank(), + isLoading = uiState.isLoading, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(Modifier.height(16.dp)) + } +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/editbankaccount/EditBankAccountViewModel.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/editbankaccount/EditBankAccountViewModel.kt new file mode 100644 index 0000000000..832af1b486 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/editbankaccount/EditBankAccountViewModel.kt @@ -0,0 +1,118 @@ +package com.hedvig.android.feature.payoutaccount.ui.editbankaccount + +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.TextFieldBuffer +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.maxLength +import androidx.compose.foundation.text.input.placeCursorAtEnd +import androidx.compose.foundation.text.input.then +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.hedvig.android.feature.payoutaccount.data.SetupNordeaPayoutUseCase +import com.hedvig.android.feature.payoutaccount.data.bankNameForClearingNumber +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope +import com.hedvig.android.molecule.public.MoleculeViewModel + +internal class EditBankAccountViewModel( + setupNordeaPayoutUseCase: SetupNordeaPayoutUseCase, +) : MoleculeViewModel( + EditBankAccountUiState(TextFieldState(), TextFieldState(), null, false, null, false), + EditBankAccountPresenter(setupNordeaPayoutUseCase), +) + +internal sealed interface EditBankAccountEvent { + data object Save : EditBankAccountEvent +} + +internal data class EditBankAccountUiState( + val clearingNumberState: TextFieldState, + val accountNumberState: TextFieldState, + val bankName: String?, + val isLoading: Boolean, + val errorMessage: String?, + val navigateBack: Boolean, +) { + // Swedish clearing numbers are 4 digits for most banks, 5 for Swedbank's 8-series + val clearingInputTransformation: InputTransformation = InputTransformation.maxLength(5).digitsOnly() + + // Swedish account numbers are up to 10 digits + val accountNumberInputTransformation: InputTransformation = InputTransformation.maxLength(10).digitsOnly() +} + +internal class EditBankAccountPresenter( + private val setupNordeaPayoutUseCase: SetupNordeaPayoutUseCase, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present( + lastState: EditBankAccountUiState, + ): EditBankAccountUiState { + val clearingNumberState = remember { lastState.clearingNumberState } + val accountNumberState = remember { lastState.accountNumberState } + val bankName = bankNameForClearingNumber(clearingNumberState.text.toString()) + var isLoading by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + var navigateBack by remember { mutableStateOf(false) } + var saveIteration by remember { mutableStateOf?>(null) } + + val currentSave = saveIteration + if (currentSave != null) { + LaunchedEffect(currentSave) { + isLoading = true + errorMessage = null + setupNordeaPayoutUseCase.invoke(currentSave.first, currentSave.second).fold( + ifLeft = { + isLoading = false + errorMessage = it.message ?: "Something went wrong, please try again" + saveIteration = null + }, + ifRight = { + isLoading = false + navigateBack = true + saveIteration = null + }, + ) + } + } + + CollectEvents { event -> + when (event) { + EditBankAccountEvent.Save -> { + if (!isLoading) { + saveIteration = clearingNumberState.text.toString() to accountNumberState.text.toString() + } + } + } + } + + return EditBankAccountUiState( + clearingNumberState = clearingNumberState, + accountNumberState = accountNumberState, + bankName = bankName, + isLoading = isLoading, + errorMessage = errorMessage, + navigateBack = navigateBack, + ) + } +} + +@Stable +private fun InputTransformation.digitsOnly(): InputTransformation = this.then(DigitsOnlyTransformation) + +private data object DigitsOnlyTransformation : InputTransformation { + override fun TextFieldBuffer.transformInput() { + val current = toString() + val filtered = current.filter { it.isDigit() } + if (filtered.length != current.length) { + replace(0, current.length, filtered) + placeCursorAtEnd() + } + } + + override fun toString(): String = "InputTransformation.DigitsOnly" +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/overview/PayoutAccountOverviewDestination.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/overview/PayoutAccountOverviewDestination.kt new file mode 100644 index 0000000000..7d21a804e9 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/overview/PayoutAccountOverviewDestination.kt @@ -0,0 +1,190 @@ +package com.hedvig.android.feature.payoutaccount.ui.overview + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +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.HedvigFullScreenCenterAlignedProgressDebounced +import com.hedvig.android.design.system.hedvig.HedvigScaffold +import com.hedvig.android.design.system.hedvig.HedvigTextField +import com.hedvig.android.design.system.hedvig.HedvigTextFieldDefaults +import com.hedvig.android.feature.payoutaccount.data.PayoutAccount +import com.hedvig.android.feature.payoutaccount.ui.overview.PayoutAccountOverviewUiState.Content +import octopus.type.MemberPaymentProvider + +@Composable +internal fun PayoutAccountOverviewDestination( + viewModel: PayoutAccountOverviewViewModel, + onConnectPayoutMethodClicked: () -> Unit, + onEditBankAccountClicked: () -> Unit, + navigateUp: () -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + PayoutAccountOverviewScreen( + uiState = uiState, + onConnectPayoutMethodClicked = onConnectPayoutMethodClicked, + onEditBankAccountClicked = onEditBankAccountClicked, + onRetry = { viewModel.emit(PayoutAccountOverviewEvent.Retry) }, + navigateUp = navigateUp, + ) +} + +@Composable +private fun PayoutAccountOverviewScreen( + uiState: PayoutAccountOverviewUiState, + onConnectPayoutMethodClicked: () -> Unit, + onEditBankAccountClicked: () -> Unit, + onRetry: () -> Unit, + navigateUp: () -> Unit, +) { + HedvigScaffold( + topAppBarText = "Payout account", + navigateUp = navigateUp, + modifier = Modifier.fillMaxSize(), + ) { + when (uiState) { + PayoutAccountOverviewUiState.Loading -> { + HedvigFullScreenCenterAlignedProgressDebounced( + Modifier + .weight(1f) + .wrapContentHeight(), + ) + } + + PayoutAccountOverviewUiState.Error -> { + HedvigErrorSection( + onButtonClick = onRetry, + modifier = Modifier + .weight(1f) + .wrapContentHeight(), + ) + } + + is Content -> { + PayoutAccountContent( + currentMethod = uiState.currentMethod, + availablePayoutMethods = uiState.availablePayoutMethods, + onConnectPayoutMethodClicked = onConnectPayoutMethodClicked, + onEditBankAccountClicked = onEditBankAccountClicked, + ) + } + } + } +} + +@Composable +private fun PayoutAccountContent( + currentMethod: PayoutAccount?, + availablePayoutMethods: List, + onConnectPayoutMethodClicked: () -> Unit, + onEditBankAccountClicked: () -> Unit, +) { + Column { + Spacer(Modifier.height(8.dp)) + when (currentMethod) { + null -> { + HedvigButton( + text = "Connect payout account", + onClick = onConnectPayoutMethodClicked, + enabled = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + + is PayoutAccount.SwishPayout -> { + HedvigTextField( + text = currentMethod.phoneNumber, + onValueChange = {}, + labelText = "Swish", + textFieldSize = HedvigTextFieldDefaults.TextFieldSize.Large, + readOnly = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + if (availablePayoutMethods.size > 1) { + Spacer(Modifier.height(8.dp)) + HedvigButton( + text = "Change account", + onClick = onConnectPayoutMethodClicked, + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Secondary, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + } + + PayoutAccount.Trustly -> { + HedvigTextField( + text = "Trustly", + onValueChange = {}, + labelText = "Account", + textFieldSize = HedvigTextFieldDefaults.TextFieldSize.Large, + readOnly = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + if (availablePayoutMethods.size > 1) { + Spacer(Modifier.height(8.dp)) + HedvigButton( + text = "Change account", + onClick = onConnectPayoutMethodClicked, + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Secondary, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + } + + is PayoutAccount.BankAccount -> { + val displayText = buildString { + if (currentMethod.bankName != null) { + append(currentMethod.bankName) + append(" ") + } + append(currentMethod.clearingNumber) + append("-") + append(currentMethod.accountNumber) + } + HedvigTextField( + text = displayText, + onValueChange = {}, + labelText = "Account", + textFieldSize = HedvigTextFieldDefaults.TextFieldSize.Large, + readOnly = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(Modifier.height(8.dp)) + HedvigButton( + text = "Edit account", + onClick = onEditBankAccountClicked, + enabled = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + } + Spacer(Modifier.height(16.dp)) + } +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/overview/PayoutAccountOverviewViewModel.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/overview/PayoutAccountOverviewViewModel.kt new file mode 100644 index 0000000000..61bfcb5b63 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/overview/PayoutAccountOverviewViewModel.kt @@ -0,0 +1,69 @@ +package com.hedvig.android.feature.payoutaccount.ui.overview + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.hedvig.android.feature.payoutaccount.data.GetPayoutAccountUseCase +import com.hedvig.android.feature.payoutaccount.data.PayoutAccount +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope +import com.hedvig.android.molecule.public.MoleculeViewModel +import octopus.type.MemberPaymentProvider + +internal class PayoutAccountOverviewViewModel( + getPayoutAccountUseCase: GetPayoutAccountUseCase, +) : MoleculeViewModel( + PayoutAccountOverviewUiState.Loading, + PayoutAccountOverviewPresenter(getPayoutAccountUseCase), + ) + +internal sealed interface PayoutAccountOverviewEvent { + data object Retry : PayoutAccountOverviewEvent +} + +internal sealed interface PayoutAccountOverviewUiState { + data object Loading : PayoutAccountOverviewUiState + + data object Error : PayoutAccountOverviewUiState + + data class Content( + val currentMethod: PayoutAccount?, + val availablePayoutMethods: List, + ) : PayoutAccountOverviewUiState +} + +internal class PayoutAccountOverviewPresenter( + private val getPayoutAccountUseCase: GetPayoutAccountUseCase, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present( + lastState: PayoutAccountOverviewUiState, + ): PayoutAccountOverviewUiState { + var loadIteration by remember { mutableStateOf(0) } + var uiState by remember { mutableStateOf(lastState) } + + LaunchedEffect(loadIteration) { + uiState = PayoutAccountOverviewUiState.Loading + getPayoutAccountUseCase.invoke().fold( + ifLeft = { uiState = PayoutAccountOverviewUiState.Error }, + ifRight = { data -> + uiState = PayoutAccountOverviewUiState.Content( + currentMethod = data.currentMethod, + availablePayoutMethods = data.availablePayoutMethods, + ) + }, + ) + } + + CollectEvents { event -> + when (event) { + PayoutAccountOverviewEvent.Retry -> loadIteration++ + } + } + + return uiState + } +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/selectmethod/SelectPayoutMethodDestination.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/selectmethod/SelectPayoutMethodDestination.kt new file mode 100644 index 0000000000..ee3ffb5d66 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/selectmethod/SelectPayoutMethodDestination.kt @@ -0,0 +1,86 @@ +package com.hedvig.android.feature.payoutaccount.ui.selectmethod + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.hedvig.android.design.system.hedvig.HedvigCard +import com.hedvig.android.design.system.hedvig.HedvigScaffold +import com.hedvig.android.design.system.hedvig.HedvigText +import octopus.type.MemberPaymentProvider + +@Composable +internal fun SelectPayoutMethodDestination( + availableProviders: List, + onTrustlySelected: () -> Unit, + onNordeaSelected: () -> Unit, + onSwishSelected: () -> Unit, + navigateUp: () -> Unit, +) { + HedvigScaffold( + topAppBarText = "Connect payout account", + navigateUp = navigateUp, + modifier = Modifier.fillMaxSize(), + ) { + Spacer(Modifier.height(8.dp)) + Column(Modifier.padding(horizontal = 16.dp)) { + for (provider in availableProviders) { + when (provider) { + MemberPaymentProvider.TRUSTLY -> { + PayoutMethodRow( + title = "Trustly", + subtitle = "Connect via Trustly", + onClick = onTrustlySelected, + ) + Spacer(Modifier.height(8.dp)) + } + MemberPaymentProvider.NORDEA -> { + PayoutMethodRow( + title = "Bank account", + subtitle = "Enter clearing and account number", + onClick = onNordeaSelected, + ) + Spacer(Modifier.height(8.dp)) + } + MemberPaymentProvider.SWISH -> { + PayoutMethodRow( + title = "Swish", + subtitle = "Connect via Swish", + onClick = onSwishSelected, + ) + Spacer(Modifier.height(8.dp)) + } + else -> {} + } + } + } + Spacer(Modifier.height(16.dp)) + } +} + +@Composable +private fun PayoutMethodRow( + title: String, + subtitle: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + HedvigCard( + onClick = onClick, + modifier = modifier.fillMaxWidth(), + ) { + Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) { + HedvigText(text = title) + HedvigText( + text = subtitle, + color = com.hedvig.android.design.system.hedvig.HedvigTheme.colorScheme.textSecondary, + ) + } + } +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/setupswish/SetupSwishPayoutDestination.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/setupswish/SetupSwishPayoutDestination.kt new file mode 100644 index 0000000000..793a12fdfd --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/setupswish/SetupSwishPayoutDestination.kt @@ -0,0 +1,93 @@ +package com.hedvig.android.feature.payoutaccount.ui.setupswish + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigNotificationCard +import com.hedvig.android.design.system.hedvig.HedvigScaffold +import com.hedvig.android.design.system.hedvig.HedvigTextField +import com.hedvig.android.design.system.hedvig.HedvigTextFieldDefaults +import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority + +@Composable +internal fun SetupSwishPayoutDestination( + viewModel: SetupSwishPayoutViewModel, + navigateUp: () -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + SetupSwishPayoutScreen( + uiState = uiState, + onSave = { viewModel.emit(SetupSwishPayoutEvent.Save) }, + navigateUp = navigateUp, + ) +} + +@Composable +private fun SetupSwishPayoutScreen( + uiState: SetupSwishPayoutUiState, + onSave: () -> Unit, + navigateUp: () -> Unit, +) { + LaunchedEffect(uiState.navigateBack) { + if (uiState.navigateBack) navigateUp() + } + + HedvigScaffold( + topAppBarText = "Swish", + navigateUp = navigateUp, + modifier = Modifier.fillMaxSize(), + ) { + Spacer(Modifier.weight(1f)) + Column(Modifier.padding(horizontal = 16.dp)) { + HedvigTextField( + state = uiState.phoneNumberState, + labelText = "Phone number", + textFieldSize = HedvigTextFieldDefaults.TextFieldSize.Large, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Phone, + ), + modifier = Modifier.fillMaxWidth(), + ) + } + AnimatedVisibility( + visible = uiState.errorMessage != null, + enter = expandVertically(), + exit = shrinkVertically(), + ) { + HedvigNotificationCard( + message = uiState.errorMessage ?: "", + priority = NotificationPriority.Attention, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 4.dp) + .fillMaxWidth(), + ) + } + Spacer(Modifier.height(16.dp)) + HedvigButton( + text = "Save", + onClick = onSave, + enabled = !uiState.isLoading && uiState.phoneNumberState.text.isNotBlank(), + isLoading = uiState.isLoading, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(Modifier.height(16.dp)) + } +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/setupswish/SetupSwishPayoutViewModel.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/setupswish/SetupSwishPayoutViewModel.kt new file mode 100644 index 0000000000..14b7419498 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/setupswish/SetupSwishPayoutViewModel.kt @@ -0,0 +1,83 @@ +package com.hedvig.android.feature.payoutaccount.ui.setupswish + +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.hedvig.android.feature.payoutaccount.data.SetupSwishPayoutUseCase +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope +import com.hedvig.android.molecule.public.MoleculeViewModel + +internal class SetupSwishPayoutViewModel( + setupSwishPayoutUseCase: SetupSwishPayoutUseCase, +) : MoleculeViewModel( + SetupSwishPayoutUiState(TextFieldState(), false, null, false), + SetupSwishPayoutPresenter(setupSwishPayoutUseCase), + ) + +internal sealed interface SetupSwishPayoutEvent { + data object Save : SetupSwishPayoutEvent +} + +internal data class SetupSwishPayoutUiState( + val phoneNumberState: TextFieldState, + val isLoading: Boolean, + val errorMessage: String?, + val navigateBack: Boolean, +) + +internal class SetupSwishPayoutPresenter( + private val setupSwishPayoutUseCase: SetupSwishPayoutUseCase, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present( + lastState: SetupSwishPayoutUiState, + ): SetupSwishPayoutUiState { + val phoneNumberState = remember { lastState.phoneNumberState } + var isLoading by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + var navigateBack by remember { mutableStateOf(false) } + var saveIteration by remember { mutableStateOf(null) } + + val currentSave = saveIteration + if (currentSave != null) { + LaunchedEffect(currentSave) { + isLoading = true + errorMessage = null + setupSwishPayoutUseCase.invoke(currentSave).fold( + ifLeft = { + isLoading = false + errorMessage = it.message ?: "Something went wrong, please try again" + saveIteration = null + }, + ifRight = { + isLoading = false + navigateBack = true + saveIteration = null + }, + ) + } + } + + CollectEvents { event -> + when (event) { + SetupSwishPayoutEvent.Save -> { + if (!isLoading) { + saveIteration = phoneNumberState.text.toString() + } + } + } + } + + return SetupSwishPayoutUiState( + phoneNumberState = phoneNumberState, + isLoading = isLoading, + errorMessage = errorMessage, + navigateBack = navigateBack, + ) + } +}