diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 316c4105eb..37c7accedd 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -4,6 +4,7 @@ on: branches: - develop - feat/pet-chip-id + - feature/payment-methods workflow_dispatch: concurrency: diff --git a/CLAUDE.md b/CLAUDE.md index 71031c0c27..834e8c60d5 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 @@ -441,6 +442,18 @@ dependencies { # String resources in app/core/core-resources/ ``` +**IMPORTANT:** String resource XML files (`strings.xml`) are fully managed by Lokalise and regenerated on every `./gradlew downloadStrings` run. **Never add new strings directly to any `strings.xml` file** — they will be overwritten and lost. + +When new UI text is needed that does not yet exist as a string resource: +1. Hardcode the English string directly in the Kotlin/Compose code. +2. Add a `// TODO: Add "" / "" to Lokalise` comment on the same line or the line above. + +Example: +```kotlin +// TODO: Add "This is some text for feature X" / "Detta är lite text för feature X" to Lokalise +Text("This is some text for feature X") +``` + ## Debugging ### Common Issues 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..99ac458a10 100644 --- a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls +++ b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls @@ -468,6 +468,41 @@ type BundleYearlySavings { """ bundleDiscountCoversFullPeriod: Boolean! } +""" +SHA-256-hashed user data for Facebook Conversions API (CAPI). +All non-null field values are hex-encoded SHA-256 hashes of the normalized plaintext. +Normalization follows https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters +""" +type CapiUserData { + """ + Hashed email (em) + """ + em: String! + """ + Hashed Gmail-normalized email, only present for Gmail addresses (gem) + """ + gem: String + """ + Hashed first name (fn) + """ + fn: String! + """ + Hashed last name (ln) + """ + ln: String! + """ + Hashed postal/zip code, null if unavailable (zp) + """ + zp: String + """ + Hashed city, null if unavailable (ct) + """ + ct: String + """ + Hashed country code, e.g. 'se' (co) + """ + co: String! +} type CarItemNotification { message: String! } @@ -1687,15 +1722,15 @@ returned as ExtendedItemDiscount """ type ExtendedItemDiscount { """ - General discount information + General discount information """ itemDiscount: ItemDiscount! """ - Monthly reduction applied by the discount. It's a negative number + Monthly reduction applied by the discount. It's a negative number """ amount: Money! """ - Whether discount is on a pending state or not + Whether discount is on a pending state or not """ isPending: Boolean! } @@ -1728,6 +1763,11 @@ type ExternalInsurer { displayName: String! insurelyId: String } +type FetchedExternalInsurance { + displayName: String! + subtitle: String + insurer: ExternalInsurer! +} type FirstVetAction { sections: [FirstVetSection!]! } @@ -2377,6 +2417,9 @@ type LinkInfo { type Location { street: String } +type ManuallyChargeMemberMutationOutput { + userError: UserError +} """ A 'Member' is the central user-like concept of our platform, referring to someone who has bought insurance with Hedvig and is now as we call is a "member". @@ -2409,6 +2452,11 @@ type Member { Payment information for this member. """ paymentInformation: MemberPaymentInformation! + """ + Null if the latest charge was successful or self manual charge is not allowed. + Id of the latest charge if it failed and self manual charge allowed. + """ + missedChargeIdToChargeManually: UUID paymentMethods: MemberPaymentMethods! conversations: [Conversation!]! legacyConversation: Conversation @@ -2419,6 +2467,8 @@ type Member { claims: [Claim!]! claimsActive: [Claim!]! claimsHistory: [Claim!]! + partnerClaimsActive: [PartnerClaim!]! + partnerClaimsHistory: [PartnerClaim!]! firstName: String! lastName: String! ssn: String @@ -2477,6 +2527,11 @@ type Member { """ crossSellV2(input: CrossSellInput!): CrossSellV2! """ + Young Pet Guide stories for the member. + Returns a list of educational content stories for young pet owners. + """ + puppyGuideStories: [PuppyGuideStory!]! + """ Fetch all the active contracts for this member. Active contracts include all insurances that are either active today, or to-be-active in the future. """ @@ -2729,6 +2784,22 @@ type MemberPaymentAvailablePaymentMethod { True if the member can set up this payment method for payout. """ supportsPayout: Boolean! + """ + True if this method is already ACTIVE for member and can be chosen as default directly without setup, false if + this is a new payment method that the member has not yet set up. + If this is true, then the `details` field will be populated with the payment method details. If this is false, then + the `details` field will be null since the member has not yet set up this payment method. + If true then this method can be set up as default directly by calling `paymentMethodSetDefaultPayin` or + `paymentMethodSetDefaultPayout` mutation depending on if it's a payin or payout method. If false, then the + corresponding mutation for setting up this payment method should be called, eg. `paymentMethodSetupTrustly`, + `paymentMethodSetupSwishPayin` etc. + """ + isActive: Boolean! + """ + For already connected and ACTIVE methods, ie isActive=true, specific details of the actual connection - e.g. a bank + account reference, phone number for swish, or email/kivra for invoice. + """ + details: PaymentMethodDetails } type MemberPaymentChargeMethodInfo { """ @@ -2801,48 +2872,76 @@ type MemberPaymentInformation { } type MemberPaymentMethod { """ - The unique id of the payment method. This id is used for switching default and revoking payment methods. - """ - id: ID! - """ - Payment provider, eg Trustly, Swish, Nordea, Kivra etc. + Payment provider, eg Trustly, Swish, Nordea, Kivra etc. + This is used as the "identifier" of the payment method since there can only be one ACTIVE or PENDING payment method + per provider. """ provider: MemberPaymentProvider! """ - The payment method status - ACTIVE, PENDING, or PENDING_DEFAULT. - PENDING_DEFAULT means the payment method is awaiting activation and will become default once activated. + The payment method status - ACTIVE, PENDING. + If ACTIVE, the payment method is ready to use for payins or payouts depending on if it's a payin or payout method. + If PENDING, the payment method has been set up but is still awaiting activation and cannot be used for payins or + payouts until then. Once activated, the status will change to ACTIVE. """ status: MemberPaymentMethodStatus! """ - True if this is the default payment method. Only one ACTIVE payment method can be default at a time. - If status is PENDING then payment method will become the default once activated. + This is 'true' for only one of the members ACTIVE methods which is the default payment method that will be used for + charging or payout the member. For PENDING methods, this can also be 'true' if the member has chosen to set up this + payment method as default during the setup process. """ isDefault: Boolean! """ - Specific details of the actual connection - e.g. a bank account reference, phone number for swish, - or email/kivra for invoice. + Specific details of the actual connection if method is ACTIVE - e.g. a bank account reference, phone number for swish, + or email/kivra for invoice. If method is PENDING, then this field will be null since the member has not yet set up + this payment method. """ - details: PaymentMethodDetails! + details: PaymentMethodDetails } type MemberPaymentMethods { """ - List of active and pending payment payin methods for this member. + List of all member's ACTIVE and PENDING payment payin methods. + A member can have multiple ACTIVE payment methods with these constraints: + - Only one ACTIVE payment method per provider, eg. one Trustly, one Swish, one Nordea etc. + - Only one ACTIVE payment method can be default at a time. + A member can have multiple PENDING payment methods with these constraints: + - Only one PENDING payment method per provider, eg. one Trustly, one Swish, one Nordea etc. + So there can exist max two payment methods per provider, one ACTIVE and one PENDING. + If a PENDING payment method has isDefault=true, then it will become the default ACTIVE payment method once activated. """ payinMethods: [MemberPaymentMethod!]! """ - List of active and pending payment payout methods for this member. + List of all member's ACTIVE and PENDING payment payout methods. + A member can have multiple ACTIVE payment methods with these constraints: + - Only one ACTIVE payment method per provider, eg. one Trustly, one Swish, one Nordea etc. + - Only one ACTIVE payment method can be default at a time. + A member can have multiple PENDING payment methods with these constraints: + - Only one PENDING payment method per provider, eg. one Trustly, one Swish, one Nordea etc. + So there can exist max two payment methods per provider, one ACTIVE and one PENDING. + If a PENDING payment method has isDefault=true, then it will become the default ACTIVE payment method once activated. """ payoutMethods: [MemberPaymentMethod!]! """ - The default payment method for payin if any. + The default payment method to use for payins if any. + Note that there can exist a PENDING payment method in `payinMethods` list with `isDefault`=true, in that case this default + payment method will be replaced by it once the pending method is activated. """ defaultPayinMethod: MemberPaymentMethod """ - The default payment method for payout if any. + The default payment method to use for payouts if any. + Note that there can exist a PENDING payment method in `payoutMethods` list with `isDefault`=true, in that case this default + payment method will be replaced by it once the pending method is activated. """ defaultPayoutMethod: MemberPaymentMethod """ - The available payment methods that the member can choose from when setting up a new payment method. + The available payment methods that the member can choose from when setting up a new payment method. + This list can include both payment methods that the member has already set up and new payment methods that the + member has not yet set up but are available to them. For already set up payment methods, the `isActive` field will + be true and the `details` field will be populated with the payment method details. For new payment methods that the + member has not yet set up, the `isActive` field will be false and the `details` field will be null. + If member picks a new payment method to set up, the corresponding mutation for setting up that payment method should + be called, eg. `paymentMethodSetupTrustly`, `paymentMethodSetupSwishPayin` etc. + If member picks an already set up payment method to set up as default, then `paymentMethodSetDefaultPayin` or + `paymentMethodSetDefaultPayout` mutation should be called depending on if it's a payin or payout method. """ availableMethods: [MemberPaymentAvailablePaymentMethod!]! """ @@ -3389,12 +3488,13 @@ input MoveToHouseInput { } type Mutation { registerDirectDebit2(clientContext: RegisterDirectDebitClientContext2): DirectDebitResponse2! + manuallyChargeMember(dueDate: Date!): ManuallyChargeMemberMutationOutput! """ Setup invoice payment method for the member. Kivra will be used as the provider if supported, else mail. """ - paymentMethodSetupInvoicePayin(input: PaymentMethodSetupInvoicePayinInput!): PaymentMethodSetupOutput! + paymentMethodSetupInvoicePayin: PaymentMethodSetupOutput! """ - Setup Trustly payment payin and payout method for the member. + Setup Trustly payment payin and payout method for the member. Requires member consent via redirect to Trustly URL in response. """ paymentMethodSetupTrustly(input: PaymentMethodSetupTrustlyInput!): PaymentMethodSetupOutput! """ @@ -3406,17 +3506,19 @@ type Mutation { """ paymentMethodSetupSwishPayout(input: PaymentMethodSetupSwishInput!): PaymentMethodSetupOutput! """ - Setup Swish payin method for the member. + Setup Swish payin method for the member. Requires member consent in Swish app. """ paymentMethodSetupSwishPayin(input: PaymentMethodSetupSwishInput!): PaymentMethodSetupOutput! """ - Revoke an active payment method. The member will be required to set up a new payment method if they revoke their default one. + A member can have multiple ACTIVE payment methods where one of those is default. This mutation changes the + members default payment method for charging to any of his/hers other active payment methods. """ - paymentMethodRevoke(id: ID!): UserError + paymentMethodSetDefaultPayin(provider: MemberPaymentProvider!): UserError """ - Set an active payment method as default. + A member can have multiple ACTIVE payment methods where one of those is default. This mutation changes the + members default payment method for payouts to any of his/hers other active payment methods. """ - paymentMethodSetDefault(id: ID!): UserError + paymentMethodSetDefaultPayout(provider: MemberPaymentProvider!): UserError """ Start a conversation. This is effectively creating one, but with two slight differences from a regular "create something"-mutation: @@ -3545,7 +3647,7 @@ type Mutation { Update the raw insurance-related data for this `PriceIntent`. This data is mostly related to the insured object itself, and not the "holder" of the insurance. """ - priceIntentDataUpdate(priceIntentId: UUID!, data: PricingFormData!): PriceIntentMutationOutput! + priceIntentDataUpdate(priceIntentId: UUID!, data: PricingFormData!, applySuggestedData: Boolean): PriceIntentMutationOutput! """ Associate a specific Insurely `dataCollectionId` from lookup-service with this PriceIntent. """ @@ -3576,6 +3678,10 @@ type Mutation { """ productOfferReprice(offerId: UUID!, data: PricingFormData!): ProductOffersMutationOutput! """ + Mark a young pet guide story as read for a specific member. + """ + puppyGuideEngagement(engagement: PuppyEngagementInput!): PuppyGuideStoryMutationOutput! + """ Update the customer of the shop session. Only non-null fields will be changed. Can trigger automatic lookup of other information. The session can be placed in a "point of no return" state where it is no longer legal to update the customer, @@ -3676,6 +3782,22 @@ type Mutation { """ upsellTravelAddonActivate(quoteId: ID!, addonId: ID!): UpsellTravelAddonActivationOutput! } +type PartnerClaim { + id: ID! + externalId: String! + exposureDisplayName: String + status: ClaimStatus + submittedAt: Date + payoutAmount: Money + associatedTypeOfContract: String + claimType: String + handlerEmail: String + displayItems: [ClaimDisplayItem!]! + """ + Terms & conditions for the claim found using claims contractId and dateOfOccurrence, otherwise null. + """ + productVariant: ProductVariant +} type PartnerData { sas: SasPartnerData } @@ -3684,7 +3806,7 @@ type PartnerWidgetTrial { } type PaymentMethodBankAccountDetails { """ - The bank account reference - e.g. clearing number and account number. + The bank account reference - e.g. clearing number + account number. """ account: String! """ @@ -3707,21 +3829,7 @@ type PaymentMethodInvoiceDetails { """ email: String } -input PaymentMethodSetupInvoicePayinInput { - """ - Set up invoice payment method as default. - """ - setAsDefaultPayout: Boolean! -} input PaymentMethodSetupNordeaPayoutInput { - """ - Set up Nordea payout method as default. - """ - setAsDefault: Boolean! - """ - The clearing number for member's bank account. - """ - clearingNumber: String! """ The account number for member's bank account. """ @@ -3732,6 +3840,10 @@ type PaymentMethodSetupOutput { The status of the setup process. If FAILED the reason for failure can be found in the `error` field. """ status: PaymentMethodSetupStatus! + """ + The order id for the payment method setup order if SUCCESSFUL. + """ + orderId: ID """ Url to redirect the member to if any. """ @@ -3756,24 +3868,12 @@ enum PaymentMethodSetupStatus { FAILED } input PaymentMethodSetupSwishInput { - """ - Set up Swish payment method as default. - """ - setAsDefault: Boolean! """ The Swish mobile number to use for payout or payin. """ phoneNumber: String! } input PaymentMethodSetupTrustlyInput { - """ - Set up Trustly payment method as default for payin. - """ - setAsDefaultPayin: Boolean! - """ - Set up Trustly payment method as default for payout. - """ - setAsDefaultPayout: Boolean! """ The URL to redirect the member back to after a successful setup after Trustly onboarding. """ @@ -3873,11 +3973,11 @@ type PriceIntent { """ product: Product! """ - Submitted user form data. + UI-safe masked form data. PII fields (street, zipCode, city) are always masked. """ data: PricingFormData! """ - Data submitted in other places or inferred from other data points + Masked, uncommitted form data from automatic lookup (e.g. SPAR address, trial data). PII fields are masked the same way as 'data'. """ suggestedData: PricingFormData! """ @@ -3905,6 +4005,15 @@ type PriceIntent { When 'true' it means user has gone trough Insurely flow with that price intent """ hasCollectedInsurelyData: Boolean! + """ + List of external insurances fetched via Insurely that correspond to products Hedvig offers. + Null when no Insurely data collection has been associated with this price intent. + """ + fetchedExternalInsurances: [FetchedExternalInsurance!] + """ + When 'true' all required form data has been provided and the price intent can be confirmed. + """ + isReadyToConfirm: Boolean! } enum PriceIntentAnimal { CAT @@ -4065,7 +4174,7 @@ type ProductOffer { """ priceIntentId: UUID """ - The form data used to generate the offer + UI-safe masked form data used to generate the offer. PII fields (street, zipCode, city) are masked when address came from registration address lookup. """ priceIntentData: PricingFormData! """ @@ -4299,6 +4408,53 @@ type ProductVariantComparisonRow { """ covered: [String!]! } +input PuppyEngagementInput { + name: String! + rating: Int + opened: Boolean + read: Boolean + closed: Boolean +} +type PuppyGuideStory { + """ + The unique name/identifier of the story. + """ + name: String! + """ + The display title of the story. + """ + title: String! + """ + The subtitle or description of the story. + """ + subtitle: String! + """ + The main content of the story. + """ + content: String! + """ + The image associated with this story. + """ + image: String! + """ + Categories this story belongs to. + """ + categories: [String!]! + """ + The date when the story was marked as read by the user. + """ + read: Boolean! + """ + The user's rating of the story. + """ + rating: Int +} +type PuppyGuideStoryMutationOutput { + """ + Indicates whether the mutation was successful. + """ + success: Boolean! +} type Query { """ Return a conversation for a given ID. @@ -4306,6 +4462,7 @@ type Query { """ conversation(id: UUID!): Conversation claim(id: ID!): Claim + partnerClaim(id: ID!): PartnerClaim claimIntent(id: ID!): ClaimIntent! claimIntentFormFieldSearch(input: ClaimIntentFormFieldSearchInput!): ClaimIntentFormFieldSearchOutput! personalInformation(input: PersonalInformationInput!): PersonalInformation @@ -4614,6 +4771,12 @@ type ShopSessionOutcome { Note that this will not contain and `PendingContract`s. """ createdContracts: [Contract!]! + """ + Pre-hashed Facebook CAPI user data (SHA-256 hex, lowercase) for this signed session. + Fields follow Meta's Conversions API customer information parameter spec. + Requires authentication (inherits MemberAccessible from the outcome). + """ + capiUserData: CapiUserData! } type ShopSessionSigning { id: UUID! diff --git a/app/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..47ebd50893 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 @@ -68,6 +68,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 @@ -184,6 +186,7 @@ internal fun HedvigNavHost( navController.navigate(ClaimDetailDestination.ClaimOverviewDestination(claimId)) }, navigateToConnectPayment = navigateToConnectPayment, + navigateToConnectPayout = { navController.navigate(PayoutAccountDestination.Graph) }, navigateToMissingInfo = { contractId: String, type: CoInsuredFlowType -> navController.navigate(CoInsuredAddInfo(contractId, type)) }, @@ -339,10 +342,21 @@ internal fun HedvigNavHost( navController = navController, hedvigDeepLinkContainer = hedvigDeepLinkContainer, navigateToConnectPayment = navigateToConnectPayment, + navigateToPayoutAccount = { navController.navigate(PayoutAccountDestination.Graph) }, languageService = languageService, hedvigBuildConstants = hedvigBuildConstants, onOpenChat = ::navigateToNewConversation, ) + payoutAccountGraph( + navController = navController, + globalSnackBarState = globalSnackBarState, + hedvigDeepLinkContainer = hedvigDeepLinkContainer, + navigateToTrustlyPayout = { builder -> + navController.navigate(TrustlyDestination, builder) + }, + navigateBack = popBackStackOrFinish, + navigateUp = navController::navigateUp, + ) profileGraph( settingsDestinationNestedGraphs = { deleteAccountGraph(hedvigDeepLinkContainer, navController) @@ -361,6 +375,7 @@ internal fun HedvigNavHost( hedvigDeepLinkContainer = hedvigDeepLinkContainer, hedvigBuildConstants = hedvigBuildConstants, navigateToConnectPayment = navigateToConnectPayment, + navigateToConnectPayout = { navController.navigate(PayoutAccountDestination.Graph) }, navigateToAddMissingInfo = { contractId: String, type: CoInsuredFlowType -> navController.navigate(CoInsuredAddInfo(contractId, type)) }, @@ -422,10 +437,10 @@ internal fun HedvigNavHost( hedvigDeepLinkContainer = hedvigDeepLinkContainer, popBackStackOrFinish = popBackStackOrFinish, goHome = { - navController.navigate(HomeDestination.Graph) { - popUpTo(ChipIdGraphDestination::class) { inclusive = true } - } - } + navController.navigate(HomeDestination.Graph) { + popUpTo(ChipIdGraphDestination::class) { inclusive = true } + } + }, ) movingFlowGraph( navController = navController, diff --git a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml index f58b1218a2..5dbfd176c7 100644 --- a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml @@ -78,6 +78,9 @@ Vi använder AI för att du ska få så snabb hjälp som möjligt. Svaren genereras automatiskt och kan ibland innehålla felaktigheter.\n\nVid behov kopplas du till vårt serviceteam som hjälper dig.\n\nVad som gäller för din försäkring anges i ditt försäkringsbrev och i dina villkor. AI och automatiska meddelanden Skanna QR-koden med BankID-appen på din telefon, eller logga in med din e-postadress nedan. + Utbetalning till ett svenskt bankkonto + Bankkonto + Clearingnummer Hej - just nu har vi lunch-stängt i chatten. Vi svarar så fort vi kan när vi öppnar igen klockan 13. Tack för ditt meddelande. Vi svarar så snart som möjligt. Just nu är chatten stängd. Vi svarar så fort vi kan när vi öppnar. @@ -143,6 +146,7 @@ Ange byggår Byggår Du + %1$d + Ändra utbetalningskonto Ändra skyddsnivå Chatta med en specialist Om det inte fungerar eller om du har några andra frågor, vänligen meddela oss så hjälper en av våra specialister till inom kort! @@ -508,8 +512,12 @@ Invalid National Identity Number Vi försöker reparera i första hand, men om din %1$s skulle behöva ersättas helt (ex. om den blivit stulen) ersätts du med **%2$d\u0025** av inköpspriset **%3$d kr**, alltså **%4$d kr**. Värdering - Du kan inte ändra betalmetod just nu. Kontakta oss för hjälp. + Vill du betala din försäkring via autogiro och möjliggöra snabba utbetalningar vid skador? Fakturan skickas till din Kivra-inkorg 14 dagar före förfallodatumet varje månad. + Tillgänglighet + Juridisk information + Legal information + Personuppgifter Fråga angående skadeanmälan - Fordon reg. %1$s Välj land och språk Logga ut @@ -600,6 +608,16 @@ Denna betalning misslyckades och lades till i din betalning den %1$s . Betalningshistorik Betalningssätt + Att betala: %1$s + Vi kunde inte dra betalningen från ditt bankkonto. Betala för att undvika avbrott i ditt skydd. + Granska betalning + Betala för att undvika avbrott + Förfallodag + Se till att det finns tillräckliga medel på kontot för att genomföra betalningen + Betala %1$s + Försenad sedan %1$s + Visa betalningsdetaljer + Försenad betalning Betalning genomförd %1$s dagar Hel period @@ -613,6 +631,13 @@ Kommande betalning Giltig till %1$s Betalningshistorik + Lägg till utbetalningsmetod + Direktutbetalning via Faktura + Snabb utbetalning med Swish + Utbetalning via Trustly + Du har inte lagt till någon utbetalningsmetod än. Lägg till en för att vi ska kunna betala ut till dig. + Utbetalningskonto + Välj utbetalningsmetod Vi behöver tillåtelse att spela in ljud Behöver tillåtelse Telefonnummer @@ -630,6 +655,7 @@ Vi kan inte radera ditt konto just nu För att vi ska kunna radera ditt konto måste alla öppna skadeärenden vara stängda och inte finnas några aktiva försäkringar. Var vänlig skriv till oss i appen. Hedvig Forever + Information Är du säker på att du vill logga ut? Epost Inkorrekt epostadress diff --git a/app/core/core-resources/src/androidMain/res/values/strings.xml b/app/core/core-resources/src/androidMain/res/values/strings.xml index f2db8a113b..22400fff12 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -78,6 +78,9 @@ We use AI to help you as quickly as possible. The answers are generated automatically and may not always be correct.\n\nIf necessary, you’ll be connected to our service team to help you further.\n\nThe details of your insurance are outlined in your insurance letter and terms and conditions. AI and automated messages Scan the QR-code with the BankID app on the phone where it’s installed or log in with email using the button below. + Payout to a Swedish bank account + Bank account + Clearing number Hej - just nu har vi lunch-stängt i chatten. Vi svarar så fort vi kan när vi öppnar igen klockan 13. Thank you for reaching out to us. We will answer as soon as possible. Hej - just nu är chatten stängd. Vi svarar så fort vi kan när vi öppnar. @@ -143,6 +146,7 @@ Please enter year of construction Year of construction You + %1$d + Change payout account Change coverage level Talk to a human If it’s not working or if you have any other questions, please let us know and one of our specialists will help out shortly! @@ -508,8 +512,12 @@ Invalid National Identity Number We first try to repair your %1$s, but if it needs to be replaced (e.g. if it was stolen) you will be compensated **%2$d\u0025** of the purchase price **%3$d SEK**, i.e **%4$d SEK**. Valuation - You can’t change the payment method right now. Contact us for help. + Do you want to pay your insurance via direct debit and enable quick payouts in the event of claims? The invoice is sent to your Kivra inbox 14 days before the due date each month. + Accessibility + Legal + Legal information + Privacy policy Question regarding claim, Vehicle reg. %1$s Preferences Logout @@ -600,6 +608,16 @@ This payment failed and was added to your payment on %1$s. Payment history Payment method + Amount due: %1$s + We couldn\'t collect this payment from your bank account. Pay now to keep your coverage active. + Review payment + Pay now to avoid interruption + Payment due + Ensure your account has enough funds to cover this payment + Pay %1$s + Overdue since %1$s + View payment details + Payment overdue Payment successful %1$s days Full period @@ -613,6 +631,13 @@ Upcoming payment Valid until %1$s Payment history + Add payout method + Direct payment via Invoice + Instant payout with Swish + Payout via Trustly + You haven’t added a payout method yet. Add one to receive payouts. + Payout account + Choose payout method We need permission to record audio Needs permission Phone @@ -630,6 +655,7 @@ We can\'t delete your account right now In order to delete your account you need to have your open claims settled and not have any active insurances. Please reach out to our service team via message in the app. Hedvig Forever + Information Are you sure you want to log out? Email Invalid email diff --git a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml index 35d531ddea..ba5e216da6 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -78,6 +78,9 @@ Vi använder AI för att du ska få så snabb hjälp som möjligt. Svaren genereras automatiskt och kan ibland innehålla felaktigheter.\n\nVid behov kopplas du till vårt serviceteam som hjälper dig.\n\nVad som gäller för din försäkring anges i ditt försäkringsbrev och i dina villkor. AI och automatiska meddelanden Skanna QR-koden med BankID-appen på din telefon, eller logga in med din e-postadress nedan. + Utbetalning till ett svenskt bankkonto + Bankkonto + Clearingnummer Hej - just nu har vi lunch-stängt i chatten. Vi svarar så fort vi kan när vi öppnar igen klockan 13. Tack för ditt meddelande. Vi svarar så snart som möjligt. Just nu är chatten stängd. Vi svarar så fort vi kan när vi öppnar. @@ -143,6 +146,7 @@ Ange byggår Byggår Du + %1$d + Ändra utbetalningskonto Ändra skyddsnivå Chatta med en specialist Om det inte fungerar eller om du har några andra frågor, vänligen meddela oss så hjälper en av våra specialister till inom kort! @@ -508,8 +512,12 @@ Invalid National Identity Number Vi försöker reparera i första hand, men om din %1$s skulle behöva ersättas helt (ex. om den blivit stulen) ersätts du med **%2$d\u0025** av inköpspriset **%3$d kr**, alltså **%4$d kr**. Värdering - Du kan inte ändra betalmetod just nu. Kontakta oss för hjälp. + Vill du betala din försäkring via autogiro och möjliggöra snabba utbetalningar vid skador? Fakturan skickas till din Kivra-inkorg 14 dagar före förfallodatumet varje månad. + Tillgänglighet + Juridisk information + Legal information + Personuppgifter Fråga angående skadeanmälan - Fordon reg. %1$s Välj land och språk Logga ut @@ -600,6 +608,16 @@ Denna betalning misslyckades och lades till i din betalning den %1$s . Betalningshistorik Betalningssätt + Att betala: %1$s + Vi kunde inte dra betalningen från ditt bankkonto. Betala för att undvika avbrott i ditt skydd. + Granska betalning + Betala för att undvika avbrott + Förfallodag + Se till att det finns tillräckliga medel på kontot för att genomföra betalningen + Betala %1$s + Försenad sedan %1$s + Visa betalningsdetaljer + Försenad betalning Betalning genomförd %1$s dagar Hel period @@ -613,6 +631,13 @@ Kommande betalning Giltig till %1$s Betalningshistorik + Lägg till utbetalningsmetod + Direktutbetalning via Faktura + Snabb utbetalning med Swish + Utbetalning via Trustly + Du har inte lagt till någon utbetalningsmetod än. Lägg till en för att vi ska kunna betala ut till dig. + Utbetalningskonto + Välj utbetalningsmetod Vi behöver tillåtelse att spela in ljud Behöver tillåtelse Telefonnummer @@ -630,6 +655,7 @@ Vi kan inte radera ditt konto just nu För att vi ska kunna radera ditt konto måste alla öppna skadeärenden vara stängda och inte finnas några aktiva försäkringar. Var vänlig skriv till oss i appen. Hedvig Forever + Information Är du säker på att du vill logga ut? Epost Inkorrekt epostadress diff --git a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml index 7edb7fd368..73bff99a7c 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -78,6 +78,9 @@ We use AI to help you as quickly as possible. The answers are generated automatically and may not always be correct.\n\nIf necessary, you’ll be connected to our service team to help you further.\n\nThe details of your insurance are outlined in your insurance letter and terms and conditions. AI and automated messages Scan the QR-code with the BankID app on the phone where it’s installed or log in with email using the button below. + Payout to a Swedish bank account + Bank account + Clearing number Hej - just nu har vi lunch-stängt i chatten. Vi svarar så fort vi kan när vi öppnar igen klockan 13. Thank you for reaching out to us. We will answer as soon as possible. Hej - just nu är chatten stängd. Vi svarar så fort vi kan när vi öppnar. @@ -143,6 +146,7 @@ Please enter year of construction Year of construction You + %1$d + Change payout account Change coverage level Talk to a human If it’s not working or if you have any other questions, please let us know and one of our specialists will help out shortly! @@ -508,8 +512,12 @@ Invalid National Identity Number We first try to repair your %1$s, but if it needs to be replaced (e.g. if it was stolen) you will be compensated **%2$d\u0025** of the purchase price **%3$d SEK**, i.e **%4$d SEK**. Valuation - You can’t change the payment method right now. Contact us for help. + Do you want to pay your insurance via direct debit and enable quick payouts in the event of claims? The invoice is sent to your Kivra inbox 14 days before the due date each month. + Accessibility + Legal + Legal information + Privacy policy Question regarding claim, Vehicle reg. %1$s Preferences Logout @@ -600,6 +608,16 @@ This payment failed and was added to your payment on %1$s. Payment history Payment method + Amount due: %1$s + We couldn't collect this payment from your bank account. Pay now to keep your coverage active. + Review payment + Pay now to avoid interruption + Payment due + Ensure your account has enough funds to cover this payment + Pay %1$s + Overdue since %1$s + View payment details + Payment overdue Payment successful %1$s days Full period @@ -613,6 +631,13 @@ Upcoming payment Valid until %1$s Payment history + Add payout method + Direct payment via Invoice + Instant payout with Swish + Payout via Trustly + You haven’t added a payout method yet. Add one to receive payouts. + Payout account + Choose payout method We need permission to record audio Needs permission Phone @@ -630,6 +655,7 @@ We can't delete your account right now In order to delete your account you need to have your open claims settled and not have any active insurances. Please reach out to our service team via message in the app. Hedvig Forever + Information Are you sure you want to log out? Email Invalid email diff --git a/app/feature/feature-connect-payment-trustly/src/main/graphql/InitiateTrustlyConnectPaymentSession.graphql b/app/feature/feature-connect-payment-trustly/src/main/graphql/InitiateTrustlyConnectPaymentSession.graphql deleted file mode 100644 index 95ada42b81..0000000000 --- a/app/feature/feature-connect-payment-trustly/src/main/graphql/InitiateTrustlyConnectPaymentSession.graphql +++ /dev/null @@ -1,6 +0,0 @@ -mutation InitiateTrustlyConnectPaymentSession($successUrl: String!, $failureUrl: String!) { - registerDirectDebit2(clientContext: { successUrl: $successUrl, failureUrl: $failureUrl }) { - url - orderId - } -} 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/StartTrustlySessionUseCase.kt b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/StartTrustlySessionUseCase.kt index 78c6c229b5..397ee1168d 100644 --- a/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/StartTrustlySessionUseCase.kt +++ b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/StartTrustlySessionUseCase.kt @@ -2,13 +2,14 @@ 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.InitiateTrustlyConnectPaymentSessionMutation +import octopus.SetupTrustlyPayoutMutation internal class StartTrustlySessionUseCase( private val apolloClient: ApolloClient, @@ -18,15 +19,19 @@ internal class StartTrustlySessionUseCase( return either { val data = apolloClient .mutation( - InitiateTrustlyConnectPaymentSessionMutation( + SetupTrustlyPayoutMutation( successUrl = trustlyCallback.successUrl, failureUrl = trustlyCallback.failureUrl, ), ) .safeExecute(::ErrorMessage) .bind() - logcat { "StartTrustlySessionUseCase received: ${data.registerDirectDebit2}" } - TrustlyInitiateProcessUrl(data.registerDirectDebit2.url) + logcat { "StartTrustlySessionUseCase received: ${data.paymentMethodSetupTrustly}" } + val url = ensureNotNull(data.paymentMethodSetupTrustly.url) { + logcat { "StartTrustlySessionUseCase received: ${data.paymentMethodSetupTrustly}" } + ErrorMessage(data.paymentMethodSetupTrustly.error?.message) + } + TrustlyInitiateProcessUrl(url) } } } diff --git a/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/TrustlyPresenter.kt b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/TrustlyPresenter.kt index 4927100483..da3269781f 100644 --- a/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/TrustlyPresenter.kt +++ b/app/feature/feature-connect-payment-trustly/src/main/kotlin/com/hedvig/android/feature/connect/payment/trustly/TrustlyPresenter.kt @@ -76,8 +76,9 @@ internal class TrustlyPresenter( if (connectingCardFailed) { return TrustlyUiState.FailedToConnectCard } - if (startSessionError != null) { - return TrustlyUiState.FailedToStartSession + val startSessionErrorValue = startSessionError + if (startSessionErrorValue != null) { + return TrustlyUiState.FailedToStartSession(startSessionErrorValue) } val browsingValue = browsing if (browsingValue != null) { @@ -105,7 +106,7 @@ internal sealed interface TrustlyUiState { data object FailedToConnectCard : TrustlyUiState - data object FailedToStartSession : TrustlyUiState + data class FailedToStartSession(val errorMessage: ErrorMessage) : TrustlyUiState data object SucceededInConnectingCard : TrustlyUiState } 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..860938bfad 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 @@ -28,6 +28,7 @@ import com.hedvig.android.composewebview.LoadingState import com.hedvig.android.composewebview.WebView import com.hedvig.android.composewebview.rememberSaveableWebViewState import com.hedvig.android.composewebview.rememberWebViewNavigator +import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.design.system.hedvig.EmptyState import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateButtonStyle.Button import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateIconStyle.SUCCESS @@ -39,12 +40,12 @@ import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.design.system.hedvig.TopAppBarWithBack 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.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 import com.hedvig.android.logger.logcat +import com.hedvig.android.molecule.public.MoleculeViewModel import com.hedvig.android.navigation.common.Destination import hedvig.resources.Res import hedvig.resources.general_done_button @@ -59,7 +60,11 @@ import org.jetbrains.compose.resources.stringResource data object TrustlyDestination : 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, @@ -106,10 +111,10 @@ private fun TrustlyScreen( ) } - TrustlyUiState.FailedToStartSession -> { + is TrustlyUiState.FailedToStartSession -> { HedvigErrorSection( onButtonClick = retryConnectingCard, - title = stringResource(Res.string.something_went_wrong), + title = uiState.errorMessage.message ?: stringResource(Res.string.something_went_wrong), subTitle = null, ) } @@ -214,13 +219,12 @@ private fun TrustlyPreview( } } -private class TrustlyUiStateProvider : - CollectionPreviewParameterProvider( - listOf( - TrustlyUiState.Browsing("", PreviewTrustlyCallback("", "")), - TrustlyUiState.Loading, - TrustlyUiState.FailedToConnectCard, - TrustlyUiState.FailedToStartSession, - TrustlyUiState.SucceededInConnectingCard, - ), - ) +private class TrustlyUiStateProvider : CollectionPreviewParameterProvider( + listOf( + TrustlyUiState.Browsing("", PreviewTrustlyCallback("", "")), + TrustlyUiState.Loading, + TrustlyUiState.FailedToConnectCard, + TrustlyUiState.FailedToStartSession(ErrorMessage("preview error message")), + TrustlyUiState.SucceededInConnectingCard, + ), +) diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeGraph.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeGraph.kt index 072b01417f..4aa80cf09e 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeGraph.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeGraph.kt @@ -24,6 +24,7 @@ fun NavGraphBuilder.homeGraph( onNavigateToNewConversation: () -> Unit, navigateToClaimDetails: (claimId: String) -> Unit, navigateToConnectPayment: () -> Unit, + navigateToConnectPayout: () -> Unit, navigateToContactInfo: () -> Unit, navigateToMissingInfo: (String, CoInsuredFlowType) -> Unit, navigateToHelpCenter: () -> Unit, @@ -54,6 +55,7 @@ fun NavGraphBuilder.homeGraph( navigateToClaimDetails(claimId) }, navigateToConnectPayment = dropUnlessResumed { navigateToConnectPayment() }, + navigateToConnectPayout = dropUnlessResumed { navigateToConnectPayout() }, navigateToMissingInfo = dropUnlessResumed { contractId, type -> navigateToMissingInfo(contractId, type) }, navigateToHelpCenter = dropUnlessResumed { navigateToHelpCenter() }, openUrl = openUrl, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 71a9ae2965..8095f75c6a 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -158,6 +158,7 @@ internal fun HomeDestination( navigateToClaimChatInDevMode: () -> Unit, onClaimDetailCardClicked: (String) -> Unit, navigateToConnectPayment: () -> Unit, + navigateToConnectPayout: () -> Unit, navigateToHelpCenter: () -> Unit, openUrl: (String) -> Unit, openCrossSellUrl: (String) -> Unit, @@ -180,6 +181,7 @@ internal fun HomeDestination( navigateToClaimChatInDevMode = navigateToClaimChatInDevMode, onClaimDetailCardClicked = onClaimDetailCardClicked, navigateToConnectPayment = navigateToConnectPayment, + navigateToConnectPayout = navigateToConnectPayout, navigateToHelpCenter = navigateToHelpCenter, openUrl = openUrl, openCrossSellUrl = openCrossSellUrl, @@ -208,6 +210,7 @@ private fun HomeScreen( navigateToClaimChatInDevMode: () -> Unit, onClaimDetailCardClicked: (String) -> Unit, navigateToConnectPayment: () -> Unit, + navigateToConnectPayout: () -> Unit, navigateToHelpCenter: () -> Unit, openUrl: (String) -> Unit, openCrossSellUrl: (String) -> Unit, @@ -276,6 +279,7 @@ private fun HomeScreen( notificationPermissionState = notificationPermissionState, onClaimDetailCardClicked = onClaimDetailCardClicked, navigateToConnectPayment = navigateToConnectPayment, + navigateToConnectPayout = navigateToConnectPayout, navigateToHelpCenter = navigateToHelpCenter, openClaimFlowSheet = startClaimBottomSheetState::show, openAppSettings = openAppSettings, @@ -423,6 +427,7 @@ private fun HomeScreenSuccess( notificationPermissionState: NotificationPermissionState, onClaimDetailCardClicked: (claimId: String) -> Unit, navigateToConnectPayment: () -> Unit, + navigateToConnectPayout: () -> Unit, navigateToHelpCenter: () -> Unit, openClaimFlowSheet: () -> Unit, openAppSettings: () -> Unit, @@ -505,6 +510,7 @@ private fun HomeScreenSuccess( MemberReminderCardsWithoutNotification( memberReminders = memberReminders, navigateToConnectPayment = navigateToConnectPayment, + navigateToConnectPayout = navigateToConnectPayout, navigateToAddMissingInfo = navigateToMissingInfo, onNavigateToNewConversation = onNavigateToNewConversation, openUrl = openUrl, @@ -801,6 +807,7 @@ private fun PreviewHomeScreen( navigateToClaimChat = {}, onClaimDetailCardClicked = {}, navigateToConnectPayment = {}, + navigateToConnectPayout = {}, navigateToHelpCenter = {}, openUrl = {}, openCrossSellUrl = {}, @@ -833,6 +840,7 @@ private fun PreviewHomeScreenWithError() { navigateToClaimChat = {}, onClaimDetailCardClicked = {}, navigateToConnectPayment = {}, + navigateToConnectPayout = {}, navigateToHelpCenter = {}, openUrl = {}, openCrossSellUrl = {}, @@ -886,6 +894,7 @@ private fun PreviewHomeScreenAllHomeTextTypes( navigateToClaimChat = {}, onClaimDetailCardClicked = {}, navigateToConnectPayment = {}, + navigateToConnectPayout = {}, navigateToHelpCenter = {}, openUrl = {}, openCrossSellUrl = {}, diff --git a/app/feature/feature-payments/build.gradle.kts b/app/feature/feature-payments/build.gradle.kts index 763237417d..0a249e7b59 100644 --- a/app/feature/feature-payments/build.gradle.kts +++ b/app/feature/feature-payments/build.gradle.kts @@ -9,58 +9,34 @@ 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) + 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) } diff --git a/app/feature/feature-payments/src/main/graphql/QueryShouldShowPayoutButton.graphql b/app/feature/feature-payments/src/main/graphql/QueryShouldShowPayoutButton.graphql new file mode 100644 index 0000000000..0de72fbf20 --- /dev/null +++ b/app/feature/feature-payments/src/main/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/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt index 99fada4411..9f98e86fc5 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/MemberCharge.kt @@ -3,13 +3,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/di/PaymentsModule.kt b/app/feature/feature-payments/src/main/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/main/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/PaymentsGraph.kt b/app/feature/feature-payments/src/main/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/main/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/GetShouldShowPayoutUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetShouldShowPayoutUseCase.kt new file mode 100644 index 0000000000..481bf68f30 --- /dev/null +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetShouldShowPayoutUseCase.kt @@ -0,0 +1,42 @@ +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 { + return@either true // todo testing delete + 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/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetShouldShowPayoutUseCaseProvider.kt b/app/feature/feature-payments/src/main/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/main/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/ui/details/PaymentDetailExpandableCard.kt b/app/feature/feature-payments/src/main/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/main/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/history/PaymentHistoryDestination.kt b/app/feature/feature-payments/src/main/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/main/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/payments/PaymentsDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt index f630e86423..6455ad1a89 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt @@ -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 @@ -92,6 +93,7 @@ import hedvig.resources.PAYMENTS_PAYMENT_DETAILS_INFO_TITLE import hedvig.resources.PAYMENTS_PAYMENT_HISTORY_BUTTON_LABEL import hedvig.resources.PAYMENTS_PROCESSING_PAYMENT import hedvig.resources.PAYMENTS_UPCOMING_PAYMENT +import hedvig.resources.PAYOUT_PAGE_HEADING import hedvig.resources.PROFILE_PAYMENT_CONNECT_DIRECT_DEBIT_TITLE import hedvig.resources.R import hedvig.resources.Res @@ -111,6 +113,7 @@ internal fun PaymentsDestination( onPaymentClicked: (id: String?) -> Unit, onDiscountClicked: () -> Unit, onPaymentHistoryClicked: () -> Unit, + onPayoutAccountClicked: () -> Unit, onMemberPaymentDetailsClicked: () -> Unit, onChangeBankAccount: () -> Unit, ) { @@ -121,6 +124,7 @@ internal fun PaymentsDestination( onChangeBankAccount = onChangeBankAccount, onDiscountClicked = onDiscountClicked, onPaymentHistoryClicked = onPaymentHistoryClicked, + onPayoutAccountClicked = onPayoutAccountClicked, onRetry = { viewModel.emit(Retry) }, onPaymentDetailsClicked = onMemberPaymentDetailsClicked, ) @@ -133,6 +137,7 @@ private fun PaymentsScreen( onChangeBankAccount: () -> Unit, onDiscountClicked: () -> Unit, onPaymentHistoryClicked: () -> Unit, + onPayoutAccountClicked: () -> Unit, onPaymentDetailsClicked: () -> Unit, onRetry: () -> Unit, ) { @@ -194,6 +199,7 @@ private fun PaymentsScreen( onChangeBankAccount = onChangeBankAccount, onDiscountClicked = onDiscountClicked, onPaymentHistoryClicked = onPaymentHistoryClicked, + onPayoutAccountClicked = onPayoutAccountClicked, onPaymentDetailsClicked = onPaymentDetailsClicked, ) Spacer(Modifier.height(16.dp)) @@ -218,6 +224,7 @@ private fun PaymentsContent( onChangeBankAccount: () -> Unit, onDiscountClicked: () -> Unit, onPaymentHistoryClicked: () -> Unit, + onPayoutAccountClicked: () -> Unit, onPaymentDetailsClicked: () -> Unit, modifier: Modifier = Modifier, ) { @@ -279,7 +286,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) { @@ -306,7 +315,7 @@ private fun PaymentsContent( is ConnectedPaymentInfo.NeedsSetup, ConnectedPaymentInfo.Unknown, is ConnectedPaymentInfo.Active, - -> { + -> { } } } @@ -375,7 +384,9 @@ private fun PaymentsListItems( uiState: PaymentsUiState, onDiscountClicked: () -> Unit, onPaymentHistoryClicked: () -> Unit, + onPayoutAccountClicked: () -> Unit, onPaymentDetailsClicked: () -> Unit, + showPayoutButton: Boolean, ) { val listItemsSideSpacingModifier = Modifier .padding(horizontal = 16.dp) @@ -433,6 +444,24 @@ private fun PaymentsListItems( ) } } + if (showPayoutButton) { + HorizontalDivider(modifier = listItemsSideSpacingModifier) + PaymentsListItem( + text = stringResource(Res.string.PAYOUT_PAGE_HEADING), + icon = { + Icon( + imageVector = HedvigIcons.PaymentOutline, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + }, + modifier = Modifier + .clickable(onClick = onPayoutAccountClicked) + .then(listItemsSideSpacingModifier) + .padding(vertical = 16.dp) + .fillMaxWidth(), + ) + } } } @@ -592,6 +621,7 @@ private fun PreviewPaymentScreen( {}, {}, {}, + {}, ) } } @@ -611,6 +641,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< "Card", "****1234", ), + showPayoutButton = true, ), ) add( @@ -627,6 +658,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< "Card", "****1234", ), + showPayoutButton = false, ), ) add( @@ -643,6 +675,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< "Card", "****1234", ), + showPayoutButton = false, ), ) add( @@ -662,6 +695,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< "Card", "****1234", ), + showPayoutButton = false, ), ) add( @@ -675,6 +709,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< upcomingPaymentInfo = NoInfo, ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.Pending, + showPayoutButton = false, ), ) add( @@ -690,6 +725,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( null, ), + showPayoutButton = false, ), ) add( @@ -705,6 +741,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( null, ), + showPayoutButton = false, ), ) add( @@ -723,6 +760,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< connectedPaymentInfo = ConnectedPaymentInfo.NeedsSetup( dueDateToConnect = System.now().plus(30.days).toLocalDateTime(TimeZone.UTC).date, ), + showPayoutButton = false, ), ) add( @@ -741,6 +779,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/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt index 18c59e81aa..21423a16dc 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsPresenter.kt @@ -7,26 +7,36 @@ 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 paymentsUiState: PaymentsUiState by remember { mutableStateOf(lastState) } var loadIteration by remember { mutableIntStateOf(0) } + var paymentOverviewResult: Either? by remember { mutableStateOf(null) } CollectEvents { event -> when (event) { @@ -35,71 +45,85 @@ internal class PaymentsPresenter( } 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, + 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.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 - } - }, - ) - }, - ) + } + 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 paymentsUiState + 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 } } @@ -118,6 +142,7 @@ internal sealed interface PaymentsUiState { val upcomingPaymentInfo: UpcomingPaymentInfo, val ongoingCharges: List, val connectedPaymentInfo: ConnectedPaymentInfo, + val showPayoutButton: Boolean, ) : PaymentsUiState { sealed interface UpcomingPayment { data object NoUpcomingPayment : UpcomingPayment 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/main/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/main/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-payout-account/build.gradle.kts b/app/feature/feature-payout-account/build.gradle.kts new file mode 100644 index 0000000000..105feaab33 --- /dev/null +++ b/app/feature/feature-payout-account/build.gradle.kts @@ -0,0 +1,34 @@ +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) + implementation(projects.navigationCore) +} 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..944ae5d726 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/graphql/GetPayoutMethods.graphql @@ -0,0 +1,28 @@ +query GetPayoutMethods { + currentMember { + paymentMethods { + payoutMethods { + provider + status + isDefault + details { + ... on PaymentMethodBankAccountDetails { + account + bank + } + ... on PaymentMethodSwishDetails { + phoneNumber + } + ... on PaymentMethodInvoiceDetails { + delivery + email + } + } + } + availableMethods { + provider + supportsPayout + } + } + } +} diff --git a/app/feature/feature-payout-account/src/main/graphql/SetupInvoicePayout.graphql b/app/feature/feature-payout-account/src/main/graphql/SetupInvoicePayout.graphql new file mode 100644 index 0000000000..dd61ac8dcf --- /dev/null +++ b/app/feature/feature-payout-account/src/main/graphql/SetupInvoicePayout.graphql @@ -0,0 +1,8 @@ +mutation SetupInvoicePayout { + paymentMethodSetupInvoicePayin { + status + error { + message + } + } +} 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..e5a48aeda0 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/graphql/SetupNordeaPayout.graphql @@ -0,0 +1,10 @@ +mutation SetupNordeaPayout($accountNumber: String!) { + paymentMethodSetupNordeaPayout( + input: { 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..3ad7b40f77 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/GetPayoutAccountUseCase.kt @@ -0,0 +1,109 @@ +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.PayoutMethod.Details.Companion.asPaymentMethodBankAccountDetails +import octopus.GetPayoutMethodsQuery.Data.CurrentMember.PaymentMethods.PayoutMethod.Details.Companion.asPaymentMethodInvoiceDetails +import octopus.GetPayoutMethodsQuery.Data.CurrentMember.PaymentMethods.PayoutMethod.Details.Companion.asPaymentMethodSwishDetails +import octopus.type.MemberPaymentMethodStatus +import octopus.type.MemberPaymentProvider + +internal data class PayoutAccountData( + val currentMethod: PayoutAccount?, + val availablePayoutMethods: List, +) + +internal class GetPayoutAccountUseCase( + private val apolloClient: ApolloClient, +) { + suspend fun invoke(): Either = either { + val result = apolloClient + .query(GetPayoutMethodsQuery()) + .fetchPolicy(FetchPolicy.NetworkOnly) + .safeExecute(::ErrorMessage) + .bind() + + val paymentMethods = result.currentMember.paymentMethods + val defaultPayoutMethod = paymentMethods.payoutMethods.firstOrNull { it.isDefault } + // todo payout feature. Return the other, non-default payout methods when we can switch to them + val currentMethod = defaultPayoutMethod?.let { method -> + val isPending = method.status == MemberPaymentMethodStatus.PENDING + when (method.provider) { + MemberPaymentProvider.SWISH -> { + val phoneNumber = method.details?.asPaymentMethodSwishDetails()?.phoneNumber + PayoutAccount.SwishPayout(phoneNumber = phoneNumber, isPending = isPending) + } + + MemberPaymentProvider.INVOICE -> { + val invoiceDetails = method.details?.asPaymentMethodInvoiceDetails() + PayoutAccount.Invoice( + delivery = invoiceDetails?.delivery, + email = invoiceDetails?.email, + isPending = isPending, + ) + } + + MemberPaymentProvider.TRUSTLY -> { + val (clearingNumber, accountNumber, bankName) = parseBankAccountDetails(method) + PayoutAccount.Trustly( + clearingNumber = clearingNumber, + accountNumber = accountNumber, + bankName = bankName, + isPending = isPending, + ) + } + + MemberPaymentProvider.NORDEA -> { + val (clearingNumber, accountNumber, bankName) = parseBankAccountDetails(method) + PayoutAccount.BankAccount( + clearingNumber = clearingNumber, + accountNumber = accountNumber, + bankName = bankName, + isPending = isPending, + ) + } + + else -> { + null + } + } + } + + val availablePayoutMethods = paymentMethods.availableMethods + .filter { it.supportsPayout } + .map { it.provider } + + PayoutAccountData( + currentMethod = currentMethod, + availablePayoutMethods = availablePayoutMethods, + ) + } +} + +private data class ParsedBankAccountDetails( + val clearingNumber: String?, + val accountNumber: String?, + val bankName: String?, +) + +private fun parseBankAccountDetails( + method: GetPayoutMethodsQuery.Data.CurrentMember.PaymentMethods.PayoutMethod, +): ParsedBankAccountDetails { + val bankAccountDetails = method.details?.asPaymentMethodBankAccountDetails() + val account = bankAccountDetails?.account + val dashIndex = account?.indexOf('-') ?: -1 + val clearingNumber = if (dashIndex >= 0) account?.substring(0, dashIndex) else account + val accountNumber = if (dashIndex >= 0) account?.substring(dashIndex + 1) else null + return ParsedBankAccountDetails( + clearingNumber = clearingNumber, + accountNumber = accountNumber, + bankName = bankAccountDetails?.bank, + ) +} 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..8a7b7b3bdc --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/PayoutAccount.kt @@ -0,0 +1,32 @@ +package com.hedvig.android.feature.payoutaccount.data + +import octopus.type.PaymentMethodInvoiceDelivery + +internal sealed interface PayoutAccount { + val isPending: Boolean + + data class Trustly( + val clearingNumber: String?, + val accountNumber: String?, + val bankName: String?, + override val isPending: Boolean, + ) : PayoutAccount + + data class SwishPayout( + val phoneNumber: String?, + override val isPending: Boolean, + ) : PayoutAccount + + data class BankAccount( + val clearingNumber: String?, + val accountNumber: String?, + val bankName: String?, + override val isPending: Boolean, + ) : PayoutAccount + + data class Invoice( + val delivery: PaymentMethodInvoiceDelivery?, + val email: String?, + override val isPending: Boolean, + ) : PayoutAccount +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/SetupInvoicePayoutUseCase.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/SetupInvoicePayoutUseCase.kt new file mode 100644 index 0000000000..5502cb9051 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/SetupInvoicePayoutUseCase.kt @@ -0,0 +1,35 @@ +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.SetupInvoicePayoutMutation +import octopus.type.PaymentMethodInvoiceDelivery +import octopus.type.PaymentMethodSetupStatus + +internal class SetupInvoicePayoutUseCase( + private val apolloClient: ApolloClient, + private val networkCacheManager: NetworkCacheManager, +) { + suspend fun invoke(): Either = either { + val result = apolloClient + .mutation(SetupInvoicePayoutMutation()) + .safeExecute(::ErrorMessage) + .bind() + + val output = result.paymentMethodSetupInvoicePayin + when (output.status) { + PaymentMethodSetupStatus.FAILED -> { + raise(ErrorMessage(output.error?.message ?: "Failed to set up invoice payout")) + } + + else -> { + networkCacheManager.clearCache() + } + } + } +} 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..fa6c01c758 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/SetupNordeaPayoutUseCase.kt @@ -0,0 +1,36 @@ +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 class SetupNordeaPayoutUseCase( + private val apolloClient: ApolloClient, + private val networkCacheManager: NetworkCacheManager, +) { + suspend fun invoke(accountNumber: String): Either = either { + val result = apolloClient + .mutation(SetupNordeaPayoutMutation(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..47bbc91c10 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/data/SetupSwishPayoutUseCase.kt @@ -0,0 +1,34 @@ +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)) + } + + 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..b7d1f52094 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/di/PayoutAccountModule.kt @@ -0,0 +1,25 @@ +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.SetupInvoicePayoutUseCase +import com.hedvig.android.feature.payoutaccount.data.SetupNordeaPayoutUseCase +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.setupinvoice.SetupInvoicePayoutViewModel +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 { GetPayoutAccountUseCase(get()) } + single { SetupNordeaPayoutUseCase(get(), get()) } + single { SetupSwishPayoutUseCase(get(), get()) } + single { SetupInvoicePayoutUseCase(get(), get()) } + viewModel { PayoutAccountOverviewViewModel(get()) } + viewModel { EditBankAccountViewModel(get()) } + viewModel { SetupSwishPayoutViewModel(get()) } + viewModel { SetupInvoicePayoutViewModel(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..6ea6e48a3a --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/navigation/PayoutAccountDestination.kt @@ -0,0 +1,28 @@ +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 + + @Serializable + data object SetupInvoicePayout : 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..14bb32cacd --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/navigation/PayoutAccountGraph.kt @@ -0,0 +1,109 @@ +package com.hedvig.android.feature.payoutaccount.navigation + +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptionsBuilder +import com.hedvig.android.design.system.hedvig.GlobalSnackBarState +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.setupinvoice.SetupInvoicePayoutDestination +import com.hedvig.android.feature.payoutaccount.ui.setupinvoice.SetupInvoicePayoutViewModel +import com.hedvig.android.feature.payoutaccount.ui.setupswish.SetupSwishPayoutDestination +import com.hedvig.android.feature.payoutaccount.ui.setupswish.SetupSwishPayoutViewModel +import com.hedvig.android.navigation.compose.navDeepLinks +import com.hedvig.android.navigation.compose.navdestination +import com.hedvig.android.navigation.compose.navgraph +import com.hedvig.android.navigation.compose.typedPopBackStack +import com.hedvig.android.navigation.compose.typedPopUpTo +import com.hedvig.android.navigation.core.HedvigDeepLinkContainer +import octopus.type.MemberPaymentProvider +import org.koin.compose.viewmodel.koinViewModel + +fun NavGraphBuilder.payoutAccountGraph( + navController: NavController, + globalSnackBarState: GlobalSnackBarState, + hedvigDeepLinkContainer: HedvigDeepLinkContainer, + navigateToTrustlyPayout: (builder: NavOptionsBuilder.() -> Unit) -> Unit, + navigateBack: () -> Unit, + navigateUp: () -> Unit, +) { + navgraph( + startDestination = PayoutAccountDestinations.Overview::class, + deepLinks = navDeepLinks(hedvigDeepLinkContainer.payout) + ) { + 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(), + ), + ) + }, + navigateBack = navigateBack, + navigateUp = navigateUp, + ) + } + + navdestination { + SelectPayoutMethodDestination( + availableProviders = this.availableProviders.map { MemberPaymentProvider.safeValueOf(it) }, + onTrustlySelected = dropUnlessResumed { + navigateToTrustlyPayout { + typedPopUpTo { + inclusive = true + } + } + }, + onNordeaSelected = dropUnlessResumed { navController.navigate(PayoutAccountDestinations.EditBankAccount) }, + onSwishSelected = dropUnlessResumed { navController.navigate(PayoutAccountDestinations.SetupSwishPayout) }, + onInvoiceSelected = dropUnlessResumed { navController.navigate(PayoutAccountDestinations.SetupInvoicePayout) }, + navigateUp = navController::navigateUp, + ) + } + + navdestination { + val viewModel: EditBankAccountViewModel = koinViewModel() + EditBankAccountDestination( + viewModel = viewModel, + globalSnackBarState = globalSnackBarState, + onSuccessfullyConnected = { + navController.typedPopBackStack(inclusive = true) + }, + navigateUp = navController::navigateUp, + ) + } + + navdestination { + val viewModel: SetupSwishPayoutViewModel = koinViewModel() + SetupSwishPayoutDestination( + viewModel = viewModel, + globalSnackBarState = globalSnackBarState, + onSuccessfullyConnected = { + navController.typedPopBackStack(inclusive = true) + }, + navigateUp = navController::navigateUp, + ) + } + + navdestination { + val viewModel: SetupInvoicePayoutViewModel = koinViewModel() + SetupInvoicePayoutDestination( + viewModel = viewModel, + globalSnackBarState = globalSnackBarState, + onSuccessfullyConnected = { + navController.typedPopBackStack(inclusive = true) + }, + 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..20b2a66903 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/editbankaccount/EditBankAccountDestination.kt @@ -0,0 +1,118 @@ +package com.hedvig.android.feature.payoutaccount.ui.editbankaccount + +import android.R.attr.priority +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.GlobalSnackBarState +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 +import hedvig.resources.BANK_PAYOUT_METHOD_CARD_TITLE +import hedvig.resources.Res +import hedvig.resources.general_save_button +import hedvig.resources.something_went_wrong +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun EditBankAccountDestination( + viewModel: EditBankAccountViewModel, + globalSnackBarState: GlobalSnackBarState, + onSuccessfullyConnected: () -> Unit, + navigateUp: () -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + EditBankAccountScreen( + uiState = uiState, + globalSnackBarState = globalSnackBarState, + onSave = { viewModel.emit(EditBankAccountEvent.Save) }, + showedSnackBar = { + viewModel.emit(EditBankAccountEvent.ShowedSnackBar) + onSuccessfullyConnected() + }, + navigateUp = navigateUp, + ) +} + +@Composable +private fun EditBankAccountScreen( + uiState: EditBankAccountUiState, + globalSnackBarState: GlobalSnackBarState, + onSave: () -> Unit, + showedSnackBar: () -> Unit, + navigateUp: () -> Unit, +) { + LaunchedEffect(uiState.showSuccessSnackBar) { + if (!uiState.showSuccessSnackBar) return@LaunchedEffect + globalSnackBarState.show("Changes saved", NotificationPriority.Campaign) + showedSnackBar() + } + + HedvigScaffold( + topAppBarText = stringResource(Res.string.BANK_PAYOUT_METHOD_CARD_TITLE), + navigateUp = navigateUp, + modifier = Modifier.fillMaxSize(), + ) { + Spacer(Modifier.weight(1f)) + Column(Modifier.padding(horizontal = 16.dp)) { + HedvigTextField( + state = uiState.accountNumberState, + labelText = buildString { + // TODO: Add "Clearing and account number" / "Clearing och kontonummer" to Lokalise + append("Clearing and account number") + if (uiState.bankName != null) { + append(" - ") + append(uiState.bankName) + } + }, + textFieldSize = HedvigTextFieldDefaults.TextFieldSize.Medium, + inputTransformation = uiState.accountNumberInputTransformation, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.fillMaxWidth(), + ) + } + AnimatedVisibility( + visible = uiState.errorMessage != null, + enter = expandVertically(), + exit = shrinkVertically(), + ) { + HedvigNotificationCard( + message = uiState.errorMessage?.ifBlank { stringResource(Res.string.something_went_wrong) }.orEmpty(), + priority = NotificationPriority.Attention, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 4.dp) + .fillMaxWidth(), + ) + } + Spacer(Modifier.height(16.dp)) + HedvigButton( + text = stringResource(Res.string.general_save_button), + onClick = onSave, + enabled = uiState.canSave, + 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..dbadc383a2 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/editbankaccount/EditBankAccountViewModel.kt @@ -0,0 +1,134 @@ +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 androidx.compose.runtime.snapshotFlow +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( + accountNumberState = TextFieldState(), + bankName = null, + isLoading = false, + errorMessage = null, + showSuccessSnackBar = false, + ), + EditBankAccountPresenter(setupNordeaPayoutUseCase), +) + +internal sealed interface EditBankAccountEvent { + data object Save : EditBankAccountEvent + + data object ShowedSnackBar : EditBankAccountEvent +} + +internal data class EditBankAccountUiState( + val accountNumberState: TextFieldState, + val bankName: String?, + val isLoading: Boolean, + val errorMessage: String?, + val showSuccessSnackBar: Boolean, +) { + val canSave: Boolean + get() = !isLoading && accountNumberState.text.length in 10..17 + + // Combined clearing and account number: 10-17 digits + val accountNumberInputTransformation: InputTransformation = InputTransformation.maxLength(17).digitsOnly() +} + +internal class EditBankAccountPresenter( + private val setupNordeaPayoutUseCase: SetupNordeaPayoutUseCase, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present( + lastState: EditBankAccountUiState, + ): EditBankAccountUiState { + val accountNumberState = remember { lastState.accountNumberState } + val bankName = bankNameForClearingNumber(accountNumberState.text.toString().take(4)) + var isLoading by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + var showSuccessSnackBar 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).fold( + ifLeft = { + isLoading = false + errorMessage = it.message ?: "" + saveIteration = null + }, + ifRight = { + isLoading = false + showSuccessSnackBar = true + saveIteration = null + }, + ) + } + } + + LaunchedEffect(accountNumberState) { + snapshotFlow { accountNumberState.text }.collect { + errorMessage = null + } + } + + CollectEvents { event -> + when (event) { + EditBankAccountEvent.Save -> { + if (!isLoading) { + saveIteration = accountNumberState.text.toString() + } + } + + EditBankAccountEvent.ShowedSnackBar -> { + showSuccessSnackBar = false + } + } + } + + return EditBankAccountUiState( + accountNumberState = accountNumberState, + bankName = bankName, + isLoading = isLoading, + errorMessage = errorMessage, + showSuccessSnackBar = showSuccessSnackBar, + ) + } +} + +@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..47f3ac80be --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/overview/PayoutAccountOverviewDestination.kt @@ -0,0 +1,323 @@ +package com.hedvig.android.feature.payoutaccount.ui.overview + +import androidx.compose.foundation.layout.Arrangement +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.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +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.HedvigErrorSection +import com.hedvig.android.design.system.hedvig.HedvigFullScreenCenterAlignedProgressDebounced +import com.hedvig.android.design.system.hedvig.HedvigNotificationCard +import com.hedvig.android.design.system.hedvig.HedvigPreview +import com.hedvig.android.design.system.hedvig.HedvigScaffold +import com.hedvig.android.design.system.hedvig.HedvigTextField +import com.hedvig.android.design.system.hedvig.HedvigTextFieldDefaults +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.feature.payoutaccount.data.PayoutAccount +import com.hedvig.android.feature.payoutaccount.ui.overview.PayoutAccountOverviewUiState.Content +import hedvig.resources.CHANGE_PAYOUT_METHOD_BUTTON_LABEL +import hedvig.resources.MY_PAYMENT_UPDATING_MESSAGE +import hedvig.resources.PAYMENTS_ACCOUNT +import hedvig.resources.PAYOUT_PAGE_HEADING +import hedvig.resources.PAYOUT_SELECT_PAYOUT_METHOD +import hedvig.resources.REFERRAL_PENDING_STATUS_LABEL +import hedvig.resources.Res +import hedvig.resources.general_back_button +import hedvig.resources.something_went_wrong +import octopus.type.MemberPaymentProvider +import octopus.type.PaymentMethodInvoiceDelivery +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun PayoutAccountOverviewDestination( + viewModel: PayoutAccountOverviewViewModel, + onConnectPayoutMethodClicked: () -> Unit, + navigateBack: () -> Unit, + navigateUp: () -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + PayoutAccountOverviewScreen( + uiState = uiState, + onConnectPayoutMethodClicked = onConnectPayoutMethodClicked, + onRetry = { viewModel.emit(PayoutAccountOverviewEvent.Retry) }, + navigateBack = navigateBack, + navigateUp = navigateUp, + ) +} + +@Composable +private fun PayoutAccountOverviewScreen( + uiState: PayoutAccountOverviewUiState, + onConnectPayoutMethodClicked: () -> Unit, + onRetry: () -> Unit, + navigateBack: () -> Unit, + navigateUp: () -> Unit, +) { + HedvigScaffold( + topAppBarText = stringResource(Res.string.PAYOUT_PAGE_HEADING), + 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, + navigateBack = navigateBack, + modifier = Modifier.weight(1f), + ) + } + } + } +} + +@Composable +private fun PayoutAccountContent( + currentMethod: PayoutAccount?, + availablePayoutMethods: List, + onConnectPayoutMethodClicked: () -> Unit, + navigateBack: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier) { + Spacer(Modifier.height(8.dp)) + when (currentMethod) { + null -> { + if (availablePayoutMethods.isEmpty()) { + Spacer(Modifier.weight(1f)) + HedvigErrorSection( + // todo copy when missing current and possible payout methods + title = stringResource(Res.string.something_went_wrong), + subTitle = null, + buttonText = stringResource(Res.string.general_back_button), + onButtonClick = navigateBack, + ) + } + } + + is PayoutAccount.SwishPayout -> { + val phoneNumber = currentMethod.phoneNumber.orEmpty() + PayoutAccountReadOnlyTextField( + label = "Swish", + text = if (currentMethod.isPending && phoneNumber.isBlank()) { + stringResource(Res.string.REFERRAL_PENDING_STATUS_LABEL) + } else { + phoneNumber + }, + ) + } + + is PayoutAccount.Trustly -> { + val accountNumber = formatBankAccountNumber(currentMethod.clearingNumber, currentMethod.accountNumber) + PayoutAccountReadOnlyTextField( + label = formatBankAccountLabel("Trustly", currentMethod.bankName), + text = if (currentMethod.isPending && accountNumber.isBlank()) { + stringResource(Res.string.REFERRAL_PENDING_STATUS_LABEL) + } else { + accountNumber + }, + ) + } + + is PayoutAccount.Invoice -> { + PayoutAccountReadOnlyTextField(label = "Account", text = "Invoice") + } + + is PayoutAccount.BankAccount -> { + val accountNumber = formatBankAccountNumber(currentMethod.clearingNumber, currentMethod.accountNumber) + PayoutAccountReadOnlyTextField( + label = formatBankAccountLabel(stringResource(Res.string.PAYMENTS_ACCOUNT), currentMethod.bankName), + text = if (currentMethod.isPending && accountNumber.isBlank()) { + stringResource(Res.string.REFERRAL_PENDING_STATUS_LABEL) + } else { + accountNumber + }, + ) + } + } + Spacer(Modifier.weight(1f)) + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + if (currentMethod?.isPending == true) { + HedvigNotificationCard( + message = stringResource(Res.string.MY_PAYMENT_UPDATING_MESSAGE), + priority = NotificationPriority.Info, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + if (availablePayoutMethods.isNotEmpty()) { + HedvigButton( + text = if (currentMethod == null) { + stringResource(Res.string.PAYOUT_SELECT_PAYOUT_METHOD) + } else { + stringResource(Res.string.CHANGE_PAYOUT_METHOD_BUTTON_LABEL) + }, + onClick = onConnectPayoutMethodClicked, + enabled = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + } + } + Spacer(Modifier.height(16.dp)) + } +} + +@Composable +private fun PayoutAccountReadOnlyTextField( + label: String, + text: String, + modifier: Modifier = Modifier, +) { + HedvigTextField( + text = text, + onValueChange = {}, + labelText = label, + textFieldSize = HedvigTextFieldDefaults.TextFieldSize.Medium, + readOnly = true, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) +} + +private fun formatBankAccountLabel(baseLabel: String, bankName: String?): String { + return if (bankName != null) "$baseLabel - $bankName" else baseLabel +} + +private fun formatBankAccountNumber(clearingNumber: String?, accountNumber: String?): String { + return when { + clearingNumber != null && accountNumber != null -> "$clearingNumber-$accountNumber" + else -> clearingNumber.orEmpty() + } +} + +@Composable +@HedvigPreview +private fun PreviewPayoutAccountOverviewScreen( + @PreviewParameter(PayoutAccountOverviewUiStateProvider::class) uiState: PayoutAccountOverviewUiState, +) { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + PayoutAccountOverviewScreen( + uiState = uiState, + onConnectPayoutMethodClicked = {}, + onRetry = {}, + navigateUp = {}, + navigateBack = {}, + ) + } + } +} + +private class PayoutAccountOverviewUiStateProvider : CollectionPreviewParameterProvider( + listOf( + PayoutAccountOverviewUiState.Loading, + PayoutAccountOverviewUiState.Error, + Content( + currentMethod = null, + availablePayoutMethods = listOf(MemberPaymentProvider.SWISH, MemberPaymentProvider.TRUSTLY), + ), + Content( + currentMethod = PayoutAccount.SwishPayout(phoneNumber = "070-123 45 67", isPending = false), + availablePayoutMethods = listOf(MemberPaymentProvider.SWISH), + ), + Content( + currentMethod = PayoutAccount.SwishPayout(phoneNumber = "070-123 45 67", isPending = false), + availablePayoutMethods = listOf(MemberPaymentProvider.SWISH, MemberPaymentProvider.TRUSTLY), + ), + Content( + currentMethod = PayoutAccount.SwishPayout(phoneNumber = null, isPending = true), + availablePayoutMethods = listOf(MemberPaymentProvider.SWISH), + ), + Content( + currentMethod = PayoutAccount.SwishPayout(phoneNumber = "070-123 45 67", isPending = true), + availablePayoutMethods = listOf(MemberPaymentProvider.SWISH), + ), + Content( + currentMethod = PayoutAccount.Trustly( + clearingNumber = "8327", + accountNumber = "12345678", + bankName = "Mock Swedbank", + isPending = false, + ), + availablePayoutMethods = listOf(MemberPaymentProvider.TRUSTLY), + ), + Content( + currentMethod = PayoutAccount.BankAccount( + clearingNumber = "3300", + accountNumber = "1234567", + bankName = "Nordea", + isPending = false, + ), + availablePayoutMethods = listOf(MemberPaymentProvider.NORDEA), + ), + Content( + currentMethod = PayoutAccount.BankAccount( + clearingNumber = null, + accountNumber = null, + bankName = null, + isPending = true, + ), + availablePayoutMethods = listOf(MemberPaymentProvider.NORDEA), + ), + Content( + currentMethod = PayoutAccount.BankAccount( + clearingNumber = "3300", + accountNumber = "1234567", + bankName = "Nordea", + isPending = true, + ), + availablePayoutMethods = listOf(MemberPaymentProvider.NORDEA), + ), + Content( + currentMethod = PayoutAccount.Invoice( + delivery = PaymentMethodInvoiceDelivery.KIVRA, + email = null, + isPending = false, + ), + availablePayoutMethods = listOf(MemberPaymentProvider.INVOICE), + ), + Content( + currentMethod = PayoutAccount.Invoice( + delivery = PaymentMethodInvoiceDelivery.MAIL, + email = "user@example.com", + isPending = false, + ), + availablePayoutMethods = listOf(MemberPaymentProvider.INVOICE, MemberPaymentProvider.TRUSTLY), + ), + ), +) 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..e7ef472f82 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/overview/PayoutAccountOverviewViewModel.kt @@ -0,0 +1,70 @@ +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.mutableIntStateOf +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 { mutableIntStateOf(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..6a2d45a71f --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/selectmethod/SelectPayoutMethodDestination.kt @@ -0,0 +1,101 @@ +package com.hedvig.android.feature.payoutaccount.ui.selectmethod + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +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 com.hedvig.android.design.system.hedvig.HedvigTheme +import hedvig.resources.BANK_PAYOUT_METHOD_CARD_DESCRIPTION +import hedvig.resources.BANK_PAYOUT_METHOD_CARD_TITLE +import hedvig.resources.PAYMENTS_INVOICE +import hedvig.resources.PAYOUT_METHOD_INVOICE_DESCRIPTION +import hedvig.resources.PAYOUT_METHOD_SWISH_DESCRIPTION +import hedvig.resources.PAYOUT_METHOD_TRUSTLY_DESCRIPTION +import hedvig.resources.PAYOUT_SELECT_PAYOUT_METHOD +import hedvig.resources.Res +import octopus.type.MemberPaymentProvider +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun SelectPayoutMethodDestination( + availableProviders: List, + onTrustlySelected: () -> Unit, + onNordeaSelected: () -> Unit, + onSwishSelected: () -> Unit, + onInvoiceSelected: () -> Unit, + navigateUp: () -> Unit, +) { + HedvigScaffold( + topAppBarText = stringResource(Res.string.PAYOUT_SELECT_PAYOUT_METHOD), + navigateUp = navigateUp, + modifier = Modifier.fillMaxSize(), + ) { + Spacer(Modifier.height(8.dp)) + Column(Modifier.padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + for (provider in availableProviders) { + when (provider) { + MemberPaymentProvider.TRUSTLY -> { + PayoutMethodRow( + title = "Trustly", + subtitle = stringResource(Res.string.PAYOUT_METHOD_TRUSTLY_DESCRIPTION), + onClick = onTrustlySelected, + ) + } + + MemberPaymentProvider.NORDEA -> { + PayoutMethodRow( + title = stringResource(Res.string.BANK_PAYOUT_METHOD_CARD_TITLE), + subtitle = stringResource(Res.string.BANK_PAYOUT_METHOD_CARD_DESCRIPTION), + onClick = onNordeaSelected, + ) + } + + MemberPaymentProvider.SWISH -> { + PayoutMethodRow( + title = "Swish", + subtitle = stringResource(Res.string.PAYOUT_METHOD_SWISH_DESCRIPTION), + onClick = onSwishSelected, + ) + } + + MemberPaymentProvider.INVOICE -> { + PayoutMethodRow( + title = stringResource(Res.string.PAYMENTS_INVOICE), + subtitle = stringResource(Res.string.PAYOUT_METHOD_INVOICE_DESCRIPTION), + onClick = onInvoiceSelected, + ) + } + + 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 = HedvigTheme.colorScheme.textSecondary, + ) + } + } +} diff --git a/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/setupinvoice/SetupInvoicePayoutDestination.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/setupinvoice/SetupInvoicePayoutDestination.kt new file mode 100644 index 0000000000..b73f09e543 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/setupinvoice/SetupInvoicePayoutDestination.kt @@ -0,0 +1,94 @@ +package com.hedvig.android.feature.payoutaccount.ui.setupinvoice + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +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.runtime.LaunchedEffect +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.GlobalSnackBarState +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.NotificationDefaults.NotificationPriority +import hedvig.resources.CONTACT_INFO_CHANGES_SAVED +import hedvig.resources.PAYMENTS_INVOICE +import hedvig.resources.Res +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun SetupInvoicePayoutDestination( + viewModel: SetupInvoicePayoutViewModel, + globalSnackBarState: GlobalSnackBarState, + onSuccessfullyConnected: () -> Unit, + navigateUp: () -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + SetupInvoicePayoutScreen( + uiState = uiState, + globalSnackBarState = globalSnackBarState, + onConnect = { viewModel.emit(SetupInvoicePayoutEvent.Connect) }, + showedSnackBar = { + viewModel.emit(SetupInvoicePayoutEvent.ShowedSnackBar) + onSuccessfullyConnected() + }, + navigateUp = navigateUp, + ) +} + +@Composable +private fun SetupInvoicePayoutScreen( + uiState: SetupInvoicePayoutUiState, + globalSnackBarState: GlobalSnackBarState, + onConnect: () -> Unit, + showedSnackBar: () -> Unit, + navigateUp: () -> Unit, +) { + val changesSaved = stringResource(Res.string.CONTACT_INFO_CHANGES_SAVED) + LaunchedEffect(uiState.showSuccessSnackBar) { + if (!uiState.showSuccessSnackBar) return@LaunchedEffect + globalSnackBarState.show(changesSaved, NotificationPriority.Campaign) + showedSnackBar() + } + + HedvigScaffold( + topAppBarText = stringResource(Res.string.PAYMENTS_INVOICE), + navigateUp = navigateUp, + modifier = Modifier.fillMaxSize(), + ) { + Spacer(Modifier.weight(1f)) + AnimatedVisibility( + visible = uiState.errorMessage != null, + enter = expandVertically(), + exit = shrinkVertically(), + ) { + HedvigNotificationCard( + message = uiState.errorMessage ?: "", + priority = NotificationPriority.Attention, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 4.dp) + .fillMaxWidth(), + ) + } + Spacer(Modifier.height(16.dp)) + HedvigButton( + text = "Connect", + onClick = onConnect, + enabled = !uiState.isLoading, + 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/setupinvoice/SetupInvoicePayoutViewModel.kt b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/setupinvoice/SetupInvoicePayoutViewModel.kt new file mode 100644 index 0000000000..478e28d66e --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/setupinvoice/SetupInvoicePayoutViewModel.kt @@ -0,0 +1,86 @@ +package com.hedvig.android.feature.payoutaccount.ui.setupinvoice + +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.SetupInvoicePayoutUseCase +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope +import com.hedvig.android.molecule.public.MoleculeViewModel + +internal class SetupInvoicePayoutViewModel( + setupInvoicePayoutUseCase: SetupInvoicePayoutUseCase, +) : MoleculeViewModel( + SetupInvoicePayoutUiState(false, null, false), + SetupInvoicePayoutPresenter(setupInvoicePayoutUseCase), + ) + +internal sealed interface SetupInvoicePayoutEvent { + data object Connect : SetupInvoicePayoutEvent + + data object ShowedSnackBar : SetupInvoicePayoutEvent +} + +internal data class SetupInvoicePayoutUiState( + val isLoading: Boolean, + val errorMessage: String?, + val showSuccessSnackBar: Boolean, +) + +internal class SetupInvoicePayoutPresenter( + private val setupInvoicePayoutUseCase: SetupInvoicePayoutUseCase, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present( + lastState: SetupInvoicePayoutUiState, + ): SetupInvoicePayoutUiState { + var isLoading by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + var showSuccessSnackBar by remember { mutableStateOf(false) } + var connectIteration by remember { mutableStateOf(0) } + var shouldConnect by remember { mutableStateOf(false) } + + if (shouldConnect) { + LaunchedEffect(connectIteration) { + isLoading = true + errorMessage = null + setupInvoicePayoutUseCase.invoke().fold( + ifLeft = { + isLoading = false + errorMessage = it.message ?: "Something went wrong, please try again" + shouldConnect = false + }, + ifRight = { + isLoading = false + showSuccessSnackBar = true + shouldConnect = false + }, + ) + } + } + + CollectEvents { event -> + when (event) { + SetupInvoicePayoutEvent.Connect -> { + if (!isLoading) { + shouldConnect = true + connectIteration++ + } + } + + SetupInvoicePayoutEvent.ShowedSnackBar -> { + showSuccessSnackBar = false + } + } + } + + return SetupInvoicePayoutUiState( + isLoading = isLoading, + errorMessage = errorMessage, + showSuccessSnackBar = showSuccessSnackBar, + ) + } +} 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..32a78385e1 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/setupswish/SetupSwishPayoutDestination.kt @@ -0,0 +1,112 @@ +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.GlobalSnackBarState +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 +import hedvig.resources.CONTACT_INFO_CHANGES_SAVED +import hedvig.resources.ODYSSEY_PHONE_NUMBER_LABEL +import hedvig.resources.Res +import hedvig.resources.TIER_FLOW_COMMIT_PROCESSING_ERROR_DESCRIPTION +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun SetupSwishPayoutDestination( + viewModel: SetupSwishPayoutViewModel, + globalSnackBarState: GlobalSnackBarState, + onSuccessfullyConnected: () -> Unit, + navigateUp: () -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + SetupSwishPayoutScreen( + uiState = uiState, + globalSnackBarState = globalSnackBarState, + onSave = { viewModel.emit(SetupSwishPayoutEvent.Save) }, + showedSnackBar = { + viewModel.emit(SetupSwishPayoutEvent.ShowedSnackBar) + onSuccessfullyConnected() + }, + navigateUp = navigateUp, + ) +} + +@Composable +private fun SetupSwishPayoutScreen( + uiState: SetupSwishPayoutUiState, + globalSnackBarState: GlobalSnackBarState, + onSave: () -> Unit, + showedSnackBar: () -> Unit, + navigateUp: () -> Unit, +) { + val changesSaved = stringResource(Res.string.CONTACT_INFO_CHANGES_SAVED) + LaunchedEffect(uiState.showSuccessSnackBar) { + if (!uiState.showSuccessSnackBar) return@LaunchedEffect + globalSnackBarState.show(changesSaved, NotificationPriority.Campaign) + showedSnackBar() + } + + HedvigScaffold( + topAppBarText = "Swish", + navigateUp = navigateUp, + modifier = Modifier.fillMaxSize(), + ) { + Spacer(Modifier.weight(1f)) + Column(Modifier.padding(horizontal = 16.dp)) { + HedvigTextField( + state = uiState.phoneNumberState, + labelText = stringResource(Res.string.ODYSSEY_PHONE_NUMBER_LABEL), + textFieldSize = HedvigTextFieldDefaults.TextFieldSize.Medium, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Phone, + ), + modifier = Modifier.fillMaxWidth(), + ) + } + AnimatedVisibility( + visible = uiState.errorMessage != null, + enter = expandVertically(), + exit = shrinkVertically(), + ) { + HedvigNotificationCard( + message = uiState.errorMessage?.message + ?: stringResource(Res.string.TIER_FLOW_COMMIT_PROCESSING_ERROR_DESCRIPTION), + 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.length >= 10, + 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..a5b84f2f02 --- /dev/null +++ b/app/feature/feature-payout-account/src/main/kotlin/com/hedvig/android/feature/payoutaccount/ui/setupswish/SetupSwishPayoutViewModel.kt @@ -0,0 +1,90 @@ +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.core.common.ErrorMessage +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 + + data object ShowedSnackBar : SetupSwishPayoutEvent +} + +internal data class SetupSwishPayoutUiState( + val phoneNumberState: TextFieldState, + val isLoading: Boolean, + val errorMessage: ErrorMessage?, + val showSuccessSnackBar: 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 showSuccessSnackBar 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 + saveIteration = null + }, + ifRight = { + isLoading = false + showSuccessSnackBar = true + saveIteration = null + }, + ) + } + } + + CollectEvents { event -> + when (event) { + SetupSwishPayoutEvent.Save -> { + if (!isLoading) { + saveIteration = phoneNumberState.text.toString() + } + } + + SetupSwishPayoutEvent.ShowedSnackBar -> { + showSuccessSnackBar = false + } + } + } + + return SetupSwishPayoutUiState( + phoneNumberState = phoneNumberState, + isLoading = isLoading, + errorMessage = errorMessage, + showSuccessSnackBar = showSuccessSnackBar, + ) + } +} diff --git a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt index 7dae99dc64..fd2a75006d 100644 --- a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt +++ b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileDestination.kt @@ -103,6 +103,7 @@ internal fun ProfileDestination( navigateToSettings: () -> Unit, navigateToCertificates: () -> Unit, navigateToConnectPayment: () -> Unit, + navigateToConnectPayout: () -> Unit, navigateToAddMissingInfo: (contractId: String, CoInsuredFlowType) -> Unit, openAppSettings: () -> Unit, openUrl: (String) -> Unit, @@ -122,6 +123,7 @@ internal fun ProfileDestination( navigateToSettings = navigateToSettings, navigateToCertificates = navigateToCertificates, navigateToConnectPayment = navigateToConnectPayment, + navigateToConnectPayout = navigateToConnectPayout, navigateToAddMissingInfo = navigateToAddMissingInfo, openAppSettings = openAppSettings, openUrl = openUrl, @@ -144,6 +146,7 @@ private fun ProfileScreen( navigateToSettings: () -> Unit, navigateToCertificates: () -> Unit, navigateToConnectPayment: () -> Unit, + navigateToConnectPayout: () -> Unit, navigateToAddMissingInfo: (contractId: String, CoInsuredFlowType) -> Unit, openAppSettings: () -> Unit, openUrl: (String) -> Unit, @@ -221,6 +224,7 @@ private fun ProfileScreen( MemberReminderCards( memberReminders = memberReminders, navigateToConnectPayment = navigateToConnectPayment, + navigateToConnectPayout = navigateToConnectPayout, navigateToAddMissingInfo = navigateToAddMissingInfo, openUrl = openUrl, notificationPermissionState = notificationPermissionState, diff --git a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileGraph.kt b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileGraph.kt index 5d152bd500..4e2fbc374d 100644 --- a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileGraph.kt +++ b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/tab/ProfileGraph.kt @@ -38,6 +38,7 @@ fun NavGraphBuilder.profileGraph( hedvigDeepLinkContainer: HedvigDeepLinkContainer, hedvigBuildConstants: HedvigBuildConstants, navigateToConnectPayment: () -> Unit, + navigateToConnectPayout: () -> Unit, navigateToAddMissingInfo: (contractId: String, CoInsuredFlowType) -> Unit, navigateToDeleteAccountFeature: () -> Unit, navigateToClaimHistory: () -> Unit, @@ -75,6 +76,7 @@ fun NavGraphBuilder.profileGraph( navController.navigate(Certificates) }, navigateToConnectPayment = dropUnlessResumed { navigateToConnectPayment() }, + navigateToConnectPayout = dropUnlessResumed { navigateToConnectPayout() }, navigateToAddMissingInfo = dropUnlessResumed { contractId: String, type: CoInsuredFlowType -> navigateToAddMissingInfo(contractId, type) }, diff --git a/app/member-reminders/member-reminders-public/src/main/graphql/QueryGetPayinMethodStatus.graphql b/app/member-reminders/member-reminders-public/src/main/graphql/QueryGetPayinMethodStatus.graphql index 18ef1ab779..bcdd51e8fc 100644 --- a/app/member-reminders/member-reminders-public/src/main/graphql/QueryGetPayinMethodStatus.graphql +++ b/app/member-reminders/member-reminders-public/src/main/graphql/QueryGetPayinMethodStatus.graphql @@ -8,5 +8,10 @@ query GetPayinMethodStatus { terminationDueToMissedPayments id } + paymentMethods { + payoutMethods { + status + } + } } } diff --git a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCase.kt b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCase.kt index 1784a4a4e2..f7c8e3a4f2 100644 --- a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCase.kt +++ b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetConnectPaymentReminderUseCase.kt @@ -44,10 +44,14 @@ internal class GetConnectPaymentReminderUseCaseImpl( return@either PaymentReminder.ShowMissingPaymentsReminder(missingPaymentsContractTerminationDate) } val payStatus = result.currentMember.paymentInformation.status - ensure(payStatus == MemberPaymentConnectionStatus.NEEDS_SETUP) { - ConnectPaymentReminderError.DomainError.AlreadySetup + if (payStatus == MemberPaymentConnectionStatus.NEEDS_SETUP) { + return@either PaymentReminder.ShowConnectPaymentReminder } - PaymentReminder.ShowConnectPaymentReminder + val isMissingPayoutMethod = result.currentMember.paymentMethods.payoutMethods.isEmpty() + if (isMissingPayoutMethod) { + return@either PaymentReminder.ShowConnectPayoutReminder + } + raise(ConnectPaymentReminderError.DomainError.AlreadySetup) }.onLeft { if (it !is ConnectPaymentReminderError.DomainError) { logcat { "GetConnectPaymentReminderUseCase failed with error:$it" } @@ -69,5 +73,7 @@ sealed interface ConnectPaymentReminderError { sealed interface PaymentReminder { data object ShowConnectPaymentReminder : PaymentReminder + data object ShowConnectPayoutReminder : PaymentReminder + data class ShowMissingPaymentsReminder(val terminationDate: LocalDate) : PaymentReminder } diff --git a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCase.kt b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCase.kt index f0de036c79..f2f39ee9c9 100644 --- a/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCase.kt +++ b/app/member-reminders/member-reminders-public/src/main/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCase.kt @@ -42,6 +42,10 @@ internal class GetMemberRemindersUseCaseImpl( MemberReminder.PaymentReminder.ConnectPayment() } + PaymentReminder.ShowConnectPayoutReminder -> { + MemberReminder.PaymentReminder.ConnectPayout() + } + is PaymentReminder.ShowMissingPaymentsReminder -> { MemberReminder.PaymentReminder.TerminationDueToMissedPayments( terminationDate = paymentReminder.terminationDate, @@ -126,6 +130,10 @@ sealed interface MemberReminder { data class ConnectPayment( override val id: String = UUID.randomUUID().toString(), ) : PaymentReminder + + data class ConnectPayout( + override val id: String = UUID.randomUUID().toString(), + ) : PaymentReminder } data class UpcomingRenewal( diff --git a/app/member-reminders/member-reminders-ui/src/main/kotlin/com/hedvig/android/memberreminders/ui/MemberReminderCards.kt b/app/member-reminders/member-reminders-ui/src/main/kotlin/com/hedvig/android/memberreminders/ui/MemberReminderCards.kt index 2018cc8ebe..33cf6fc4d2 100644 --- a/app/member-reminders/member-reminders-ui/src/main/kotlin/com/hedvig/android/memberreminders/ui/MemberReminderCards.kt +++ b/app/member-reminders/member-reminders-ui/src/main/kotlin/com/hedvig/android/memberreminders/ui/MemberReminderCards.kt @@ -48,6 +48,8 @@ import hedvig.resources.CONTRACT_VIEW_CERTIFICATE_BUTTON import hedvig.resources.DASHBOARD_RENEWAL_PROMPTER_BODY import hedvig.resources.MISSING_CONTACT_INFO_CARD_BUTTON import hedvig.resources.MISSING_CONTACT_INFO_CARD_TEXT +import hedvig.resources.PAYOUT_ADD_PAYOUT_METHOD +import hedvig.resources.PAYOUT_MISSING_INFO import hedvig.resources.PROFILE_ALLOW_NOTIFICATIONS_INFO_LABEL import hedvig.resources.PROFILE_PAYMENT_CONNECT_DIRECT_DEBIT_BUTTON import hedvig.resources.PUSH_NOTIFICATIONS_ALERT_ACTION_NOT_NOW @@ -73,6 +75,9 @@ fun getMemberReminderMessage(reminder: MemberReminder): String { is MemberReminder.PaymentReminder.ConnectPayment -> stringResource(Res.string.info_card_missing_payment_body) + is MemberReminder.PaymentReminder.ConnectPayout -> + stringResource(Res.string.PAYOUT_MISSING_INFO) + is MemberReminder.PaymentReminder.TerminationDueToMissedPayments -> stringResource(Res.string.info_card_missing_payment_missing_payments_body, reminder.terminationDate) @@ -126,6 +131,7 @@ fun rememberMaxLineCountForReminders( fun MemberReminderCardsWithoutNotification( memberReminders: List, navigateToConnectPayment: () -> Unit, + navigateToConnectPayout: () -> Unit, openUrl: (String) -> Unit, navigateToAddMissingInfo: (String, CoInsuredFlowType) -> Unit, onNavigateToNewConversation: () -> Unit, @@ -137,6 +143,7 @@ fun MemberReminderCardsWithoutNotification( MemberReminderCards( memberReminders = memberReminders, navigateToConnectPayment = navigateToConnectPayment, + navigateToConnectPayout = navigateToConnectPayout, openUrl = openUrl, navigateToAddMissingInfo = navigateToAddMissingInfo, onNavigateToNewConversation = onNavigateToNewConversation, @@ -153,6 +160,7 @@ fun MemberReminderCardsWithoutNotification( fun MemberReminderCards( memberReminders: List, navigateToConnectPayment: () -> Unit, + navigateToConnectPayout: () -> Unit, openUrl: (String) -> Unit, navigateToAddMissingInfo: (String, CoInsuredFlowType) -> Unit, snoozeNotificationPermissionReminder: () -> Unit, @@ -169,6 +177,7 @@ fun MemberReminderCards( memberReminder = memberReminders.first(), navigateToAddMissingInfo = navigateToAddMissingInfo, navigateToConnectPayment = navigateToConnectPayment, + navigateToConnectPayout = navigateToConnectPayout, openUrl = openUrl, onNavigateToNewConversation = onNavigateToNewConversation, snoozeNotificationPermissionReminder = snoozeNotificationPermissionReminder, @@ -200,6 +209,7 @@ fun MemberReminderCards( memberReminder = memberReminders[page], navigateToAddMissingInfo = navigateToAddMissingInfo, navigateToConnectPayment = navigateToConnectPayment, + navigateToConnectPayout = navigateToConnectPayout, openUrl = openUrl, onNavigateToNewConversation = onNavigateToNewConversation, snoozeNotificationPermissionReminder = snoozeNotificationPermissionReminder, @@ -232,6 +242,7 @@ private fun ColumnScope.MemberReminderCard( memberReminder: MemberReminder, navigateToAddMissingInfo: (String, CoInsuredFlowType) -> Unit, navigateToConnectPayment: () -> Unit, + navigateToConnectPayout: () -> Unit, navigateToContactInfo: () -> Unit, navigateToChipId: () -> Unit, openUrl: (String) -> Unit, @@ -262,6 +273,15 @@ private fun ColumnScope.MemberReminderCard( ) } + is MemberReminder.PaymentReminder.ConnectPayout -> { + ReminderCardConnectPayout( + navigateToConnectPayout = navigateToConnectPayout, + modifier = modifier, + minLines = minLines, + memberReminder = memberReminder, + ) + } + is MemberReminder.PaymentReminder.TerminationDueToMissedPayments -> { ReminderCardMissingPayment( memberReminder = memberReminder, @@ -405,6 +425,26 @@ private fun ReminderCardConnectPayment( ) } +@Composable +private fun ReminderCardConnectPayout( + memberReminder: MemberReminder, + navigateToConnectPayout: () -> Unit, + modifier: Modifier = Modifier, + minLines: Int = 1, +) { + val message = getMemberReminderMessage(memberReminder) + HedvigNotificationCard( + message = message, + modifier = modifier, + priority = NotificationPriority.Attention, + style = InfoCardStyle.Button( + buttonText = stringResource(Res.string.PAYOUT_ADD_PAYOUT_METHOD), + onButtonClick = navigateToConnectPayout, + ), + minLines = minLines, + ) +} + @Composable private fun ReminderCardMissingPayment( memberReminder: MemberReminder, diff --git a/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt b/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt index 650003a628..17b59f672a 100644 --- a/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt +++ b/app/navigation/navigation-core/src/commonMain/kotlin/com/hedvig/android/navigation/core/HedvigDeepLinkContainer.kt @@ -43,6 +43,7 @@ interface HedvigDeepLinkContainer { val directDebit: List // Same as connectPayment but to support an old link to it val eurobonus: List // The destination allowing to edit your current Eurobonus (SAS) number val payments: List // The payments screen, showing the payments history and the upcoming payment information + val payout: List // Payout connection overview screen, can see existing options and change/add payout options val deleteAccount: List // The screen where the member may request for their account data to be GDPR wiped // The screen where one can change their contact information, like their email and phone. @@ -141,6 +142,7 @@ internal class HedvigDeepLinkContainerImpl( "$baseDeepLinkDomain/eurobonus" } override val payments: List = baseDeepLinkDomains.map { baseDeepLinkDomain -> "$baseDeepLinkDomain/payments" } + override val payout: List = baseDeepLinkDomains.map { baseDeepLinkDomain -> "$baseDeepLinkDomain/payout" } override val deleteAccount: List = baseDeepLinkDomains.map { baseDeepLinkDomain -> "$baseDeepLinkDomain/delete-account" } @@ -200,39 +202,40 @@ internal class HedvigDeepLinkContainerImpl( val HedvigDeepLinkContainer.allDeepLinkUriPatterns: List get() = listOf( - home.first(), - helpCenter.first(), - helpCenterCommonTopic.first(), - helpCenterQuestion.first(), - insurances.first(), + carAddon.first(), + carAddonWithContractId.first(), + changeTierWithContractId.first(), + changeTierWithoutContractId.first(), + chat.first(), + claimDetails.first(), + claimFlow.first(), + connectPayment.first(), + contactInfo.first(), contract.first(), contractWithoutContractId.first(), + conversation.first(), + deleteAccount.first(), + directDebit.first(), editCoInsured.first(), editCoInsuredWithoutContractId.first(), - terminateInsurance.first(), - forever.first(), - profile.first(), - connectPayment.first(), - directDebit.first(), + editCoOwners.first(), eurobonus.first(), - payments.first(), - deleteAccount.first(), - contactInfo.first(), - chat.first(), + forever.first(), + helpCenter.first(), + helpCenterCommonTopic.first(), + helpCenterQuestion.first(), + home.first(), inbox.first(), - conversation.first(), - travelAddon.first(), - travelCertificate.first(), - changeTierWithoutContractId.first(), - changeTierWithContractId.first(), - claimDetails.first(), insuranceEvidence.first(), - claimFlow.first(), + insurances.first(), moveContract.first(), - editCoOwners.first(), - carAddon.first(), - carAddonWithContractId.first(), - travelAddonWithContractId.first(), - petIdWithoutContractId.first(), + payments.first(), + payout.first(), petIdWithContractId.first(), - ) + petIdWithoutContractId.first(), + profile.first(), + terminateInsurance.first(), + travelAddon.first(), + travelAddonWithContractId.first(), + travelCertificate.first(), + ) diff --git a/app/ui/pullrefresh/build.gradle.kts b/app/ui/pullrefresh/build.gradle.kts index affe7b8740..7bec49f8ed 100644 --- a/app/ui/pullrefresh/build.gradle.kts +++ b/app/ui/pullrefresh/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") } @@ -7,8 +8,12 @@ hedvig { compose() } -dependencies { - implementation(libs.androidx.compose.animationCore) - implementation(libs.jetbrains.compose.runtime) - implementation(projects.designSystemHedvig) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.jetbrains.compose.animation.core) + implementation(libs.jetbrains.compose.runtime) + implementation(projects.designSystemHedvig) + } + } } diff --git a/app/ui/pullrefresh/src/main/kotlin/com/hedvig/android/pullrefresh/PullRefresh.kt b/app/ui/pullrefresh/src/commonMain/kotlin/com/hedvig/android/pullrefresh/PullRefresh.kt similarity index 100% rename from app/ui/pullrefresh/src/main/kotlin/com/hedvig/android/pullrefresh/PullRefresh.kt rename to app/ui/pullrefresh/src/commonMain/kotlin/com/hedvig/android/pullrefresh/PullRefresh.kt diff --git a/app/ui/pullrefresh/src/main/kotlin/com/hedvig/android/pullrefresh/PullRefreshIndicator.kt b/app/ui/pullrefresh/src/commonMain/kotlin/com/hedvig/android/pullrefresh/PullRefreshIndicator.kt similarity index 100% rename from app/ui/pullrefresh/src/main/kotlin/com/hedvig/android/pullrefresh/PullRefreshIndicator.kt rename to app/ui/pullrefresh/src/commonMain/kotlin/com/hedvig/android/pullrefresh/PullRefreshIndicator.kt diff --git a/app/ui/pullrefresh/src/main/kotlin/com/hedvig/android/pullrefresh/PullRefreshIndicatorTransform.kt b/app/ui/pullrefresh/src/commonMain/kotlin/com/hedvig/android/pullrefresh/PullRefreshIndicatorTransform.kt similarity index 100% rename from app/ui/pullrefresh/src/main/kotlin/com/hedvig/android/pullrefresh/PullRefreshIndicatorTransform.kt rename to app/ui/pullrefresh/src/commonMain/kotlin/com/hedvig/android/pullrefresh/PullRefreshIndicatorTransform.kt diff --git a/app/ui/pullrefresh/src/main/kotlin/com/hedvig/android/pullrefresh/PullRefreshState.kt b/app/ui/pullrefresh/src/commonMain/kotlin/com/hedvig/android/pullrefresh/PullRefreshState.kt similarity index 100% rename from app/ui/pullrefresh/src/main/kotlin/com/hedvig/android/pullrefresh/PullRefreshState.kt rename to app/ui/pullrefresh/src/commonMain/kotlin/com/hedvig/android/pullrefresh/PullRefreshState.kt diff --git a/app/ui/pullrefresh/src/main/AndroidManifest.xml b/app/ui/pullrefresh/src/main/AndroidManifest.xml deleted file mode 100644 index 568741e54f..0000000000 --- a/app/ui/pullrefresh/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file