diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 41bbee29b1..c88afabfcb 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -4,6 +4,7 @@ on: branches: - develop - feat/manual-charge + - feat/puppy-guide-kmp workflow_dispatch: concurrency: diff --git a/.github/workflows/umbrella.yml b/.github/workflows/umbrella.yml index 72ea7f3d2a..add3e8746d 100644 --- a/.github/workflows/umbrella.yml +++ b/.github/workflows/umbrella.yml @@ -4,11 +4,8 @@ on: push: branches: - develop - - makerdays/chat-claim - paths: - - 'app/authlib/**' - - 'app/feature/feature-claim-chat/**' - - 'app/shareddi/**' + - feat/help-center-kmp + - feat/puppy-guide-kmp workflow_dispatch: concurrency: diff --git a/CLAUDE.md b/CLAUDE.md index 834e8c60d5..e7d6c72902 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -221,6 +221,43 @@ data-{domain}/ - Use cases for business logic - Room database for local persistence +**Critical architectural rule — never expose GraphQL types in public API:** + +GraphQL is an implementation detail of the data layer. Apollo-generated types (anything from the `octopus` package — queries, mutations, fragments, their `.Data` shapes, generated input/enum types, etc.) **must not appear in the signatures of public interfaces, public functions, return types, or public data classes** that other modules consume. + +Use cases and repositories should: +1. Run the GraphQL operation internally (`.query(...)`, `.mutation(...)`, `.safeExecute()`, `.safeFlow()`). +2. Map the response into a project-owned type (a plain Kotlin `data class`, sealed type, primitive, or `Unit` if only success/failure matters) before returning. +3. Keep the `octopus.*` import confined to the `internal` impl class only. + +This applies even when the GraphQL type happens to be a perfect shape — wrap it. It keeps the rest of the project insulated from schema churn, makes the data source swappable, and prevents GraphQL types from leaking into KMP/iOS-facing APIs where they'd be even more awkward. + +Example — wrong: +```kotlin +interface SetArticleRatingUseCase { + // ❌ exposes Apollo-generated type + suspend fun invoke(name: String, rating: Int): Either +} +``` + +Example — right: +```kotlin +interface SetArticleRatingUseCase { + // ✅ project-owned shape; Unit because callers only care about success/failure + suspend fun invoke(name: String, rating: Int): Either +} + +internal class SetArticleRatingUseCaseImpl(...) : SetArticleRatingUseCase { + override suspend fun invoke(...) = either { + val data = apolloClient.mutation(PuppyGuideEngagementMutation(...)).safeExecute() + .mapLeft { ErrorMessage() }.bind() + ensure(data.puppyGuideEngagement.success) { ErrorMessage() } + } +} +``` + +When the response carries useful structured data, define a project-owned `data class` next to the use case (or in a shared model file) and map field-by-field in the impl. + ## Technology Stack ### UI diff --git a/README.md b/README.md index fff176b208..e72fb40876 100644 --- a/README.md +++ b/README.md @@ -45,4 +45,15 @@ The [android-remove-unused-resources-plugin](https://github.com/irgaly/android-r plugin is used to achieve this. This will run on CI using [this task](./.github/workflows/unused-resources.yml), but to run locally one can also just do `./gradlew :app:lint -Prur.lint.onlyUnusedResources` And then -`./gradlew :app:removeUnusedResourcesDebug` \ No newline at end of file +`./gradlew :app:removeUnusedResourcesDebug` + +## Sharing code with iOS via HedvigShared + +The `:umbrella` module produces the binary the iOS app consumes for shared KMP code — exported module list in `app/umbrella/build.gradle.kts`. Two distribution paths: + +- **Production**: `umbrella.yml` runs `:umbrella:assembleHedvigSharedReleaseXCFramework` and publishes the resulting multi-slice XCFramework as a Swift Package (~25 min round-trip). +- **Local dev**: Ugglan's `scripts/use-local-umbrella.sh` flips its Tuist project so a pre-build phase calls `:umbrella:embedAndSignAppleFrameworkForXcode` from a sibling `android/` checkout on every Xcode build (~5–10s per Kotlin change). See the *Iterating on shared KMP code* section in Ugglan's README. + +(`:umbrella:embedAndSignAppleFrameworkForXcode` only works from an Xcode build phase — it reads `CONFIGURATION`, `SDK_NAME`, `ARCHS`, `BUILT_PRODUCTS_DIR` from the env.) + +`:umbrella` is `isStatic = true`. Compose Resources you add to shared modules land at `.app/compose-resources/composeResources/...` on iOS via `ugglan/scripts/post-build-action.sh` — that script is the source of truth for the iOS bundle layout. \ No newline at end of file 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 077a36445e..c0d3aabc80 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 @@ -917,6 +917,20 @@ Root type expressing the entire flow for members trying to report a new claim. """ type ClaimIntent { id: ID! + """ + The time this was started. + """ + createdAt: DateTime! + """ + All steps that have been taken before the current step. These typically include "current value" data. + WARNING: Only query these once when resuming an old intent, as they can be quite expensive to compute. + They effectively need to perform all the work of rendering every previous step all at once, which we should + generally avoid when going through the flow normally. + """ + previousSteps: [ClaimIntentStep!]! + """ + The current step. + """ currentStep: ClaimIntentStep """ A value between 0.0 and 1.0 that indicates roughly how far along the submission @@ -924,6 +938,10 @@ type ClaimIntent { """ progress: Float """ + A short summary of the contents of this flow. Null when the flow is completely new (typically pre triage). + """ + displayName: String + """ The created claim of this intent. Once this value is returned, the intent is effectively over, and no steps can be produced or interacted with. @@ -964,7 +982,7 @@ type ClaimIntentSourceMessage { text: String! } input ClaimIntentStartInput { - developmentFlow: Boolean + developmentFlow: Boolean @deprecated(reason: "Unused") } """ Represents a single step in the claim submission flow. @@ -993,6 +1011,14 @@ type ClaimIntentStepContentAudioRecording { uploadUri: String! freeTextMinLength: Int! freeTextMaxLength: Int! + """ + URL of the audio uploaded by the user. + """ + currentAudioUrl: Url + """ + Free text submitted by the user. + """ + currentFreeText: String isSkippable: Boolean! } """ @@ -1038,8 +1064,17 @@ type ClaimIntentStepContentFileUpload { The relative URI towards where one should upload the files. """ uploadUri: String! + """ + Current files uploaded by the user. + """ + currentFiles: [ClaimIntentStepContentFileUploadFile!] isSkippable: Boolean! } +type ClaimIntentStepContentFileUploadFile { + url: Url! + contentType: String! + fileName: String! +} """ A form step is one that contains a form in the shape of a list of "fields". Submitted using `Mutation.claimIntentSubmitForm`. @@ -1082,6 +1117,10 @@ type ClaimIntentStepContentFormField { """ defaultValues: [ClaimIntentStepContentFormFieldValue!]! """ + These are the values that the user has submitted for this field. + """ + currentValues: [ClaimIntentStepContentFormFieldValue!]! + """ If type is SEARCH, then this will be present. """ searchData: ClaimIntentStepContentFormFieldSearchData @@ -1136,8 +1175,21 @@ It can be seen as a special-case form with nicer rendering. Submitted using `Mutation.claimIntentSubmitSelect`. """ type ClaimIntentStepContentSelect { + """ + Selectable options. + """ options: [ClaimIntentStepContentSelectOption!]! + """ + Visual style of this selection. + """ style: ClaimIntentStepContentSelectStyle! + """ + Currently selected value by the user. + """ + currentSelectedId: ID + """ + Pre-filled default value. + """ defaultSelectedId: ID isSkippable: Boolean! } @@ -2468,6 +2520,11 @@ type Member { List of members FAQ """ memberFAQ: MemberFAQ! + """ + Returns the most recent claim intent for this member, that can be "resumed" by querying `ClaimIntent.previousSteps`. + Will return `null` if the server considers it too old to be resumed. + """ + resumableClaimIntent: ClaimIntent claims: [Claim!]! claimsActive: [Claim!]! claimsHistory: [Claim!]! @@ -2531,10 +2588,16 @@ type Member { """ crossSellV2(input: CrossSellInput!): CrossSellV2! """ - Young Pet Guide stories for the member. - Returns a list of educational content stories for young pet owners. + Shop sessions started by this member that have not yet been signed and are not expired. + Ordered by most recent activity first. + """ + ongoingShopSessions: [ShopSession!]! """ - puppyGuideStories: [PuppyGuideStory!]! + Young Pet Guide content for the member. + Returns the educational content stories plus an indicator of whether the audience + is a young dog. Returns null when the member has no active dog contracts. + """ + puppyGuide: PuppyGuide """ 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. @@ -4388,6 +4451,11 @@ type ProductRecommendation { Null when no Insurely data collection is associated with the session. """ externalInsurance: RecommendationExternalInsurance + """ + Best-effort prefill for the product's price calculator, derived from Insurely data, for + product-only recommendations where no offer could be generated. Null when no usable prefill exists. + """ + priceIntentData: PricingFormData } type ProductVariant { """ @@ -4460,6 +4528,19 @@ input PuppyEngagementInput { read: Boolean closed: Boolean } +type PuppyGuide { + """ + Educational content stories for dog owners. The same set is served regardless of dog age. + """ + stories: [PuppyGuideStory!] + """ + True if the member has at least one active dog contract for a dog under 2 years old. + Clients should use this to render the content appropriately for the audience — + content tailored to puppies will be a better fit when this is true. + [null] assumes it's not a young dog, as we do not have this info + """ + forYoungDog: Boolean +} type PuppyGuideStory { """ The unique name/identifier of the story. @@ -4682,6 +4763,11 @@ type ShopSession { """ cart: Cart! """ + Display-ready summary of this session for surfacing it in member-facing lists + (e.g. the Hedvig mobile app's home screen). + """ + display: ShopSessionDisplay! + """ The outcome of a signed shop-session. Requires authentication. """ outcome: ShopSessionOutcome @@ -4816,6 +4902,49 @@ input ShopSessionCustomerUpdateInput { firstName: String lastName: String } +type ShopSessionDisplay { + """ + Title summarizing the session's products, e.g. "Car Insurance Full" for a single + product or "Home + Accident" when several products are in play. Uses each product's + full localized display name (tier-specific where applicable). + """ + title: String! + """ + Optional secondary line with insured-object info, e.g. "Studio apartment, Stockholm". + Null when no insured-object data is available. + """ + subtitle: String + """ + Effective monthly price after any campaign/bundle discounts. + Null when the session has no priced offers in the cart. + """ + monthlyNet: Money + """ + Original monthly price before any campaign/bundle discounts. + Null under the same conditions as `monthlyNet`. + """ + monthlyGross: Money + """ + Most recent activity timestamp on this session. + Used for sorting or displaying it relatively (e.g. "2 days ago"). + """ + lastActivityAt: DateTime! + """ + Absolute URL to open in a browser to resume this session. Clients open it + directly in the device browser, no auth is required. + """ + resumeUrl: String! + """ + Mirrors the underlying session's `validTo`. Clients can use this to hide cards + that are expired. + """ + validTo: DateTime! + """ + Pillow image of the primary product in the session, suitable as a card icon. + Null when no product is yet associated with the session. + """ + pillowImage: StoryblokImageAsset +} """ A/B testing experiment configuration for shop sessions. Both name and variant should be in SCREAMING_SNAKE_CASE format. diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index d6ea697d9e..55ccffe406 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -194,8 +194,7 @@ dependencies { implementation(projects.featureCrossSellSheet) implementation(projects.featureDeleteAccount) implementation(projects.featureEditCoinsured) - implementation(projects.featureFlagsAndroid) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.featureForever) implementation(projects.featureHelpCenter) implementation(projects.featureHome) 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 ff9b0703c5..db5cb4dd0e 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 @@ -360,7 +360,7 @@ val applicationModule = module { ) } -private class AndroidBuildConfig() : AppBuildConfig { +private class AndroidBuildConfig : AppBuildConfig { override val debug: Boolean = BuildConfig.DEBUG override val applicationId: String = BuildConfig.APPLICATION_ID override val buildType: String = BuildConfig.BUILD_TYPE 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 aff77558a5..69c900ed1d 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 @@ -348,7 +348,7 @@ internal fun HedvigNavHost( hedvigBuildConstants = hedvigBuildConstants, openConversation = { navigateToNewConversation() - } + }, ) payoutAccountGraph( navController = navController, @@ -456,6 +456,7 @@ internal fun HedvigNavHost( helpCenterGraph( hedvigDeepLinkContainer = hedvigDeepLinkContainer, navController = navController, + onNavigateUp = navController::navigateUp, onNavigateToQuickLink = onNavigateToQuickLink@{ quickLinkDestination -> val destination: Destination = when (quickLinkDestination) { QuickLinkChangeAddress -> { diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt index fc140e9a50..d34ca0db3b 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt @@ -39,7 +39,6 @@ import com.hedvig.android.core.demomode.Provider import com.hedvig.android.data.paying.member.GetOnlyHasNonPayingContractsUseCase import com.hedvig.android.data.settings.datastore.SettingsDataStore import com.hedvig.android.feature.cross.sell.sheet.CrossSellSheet -import com.hedvig.android.notification.badge.data.payment.MissedPaymentNotificationServiceProvider import com.hedvig.android.feature.login.navigation.LoginDestination import com.hedvig.android.featureflags.FeatureManager import com.hedvig.android.language.LanguageService @@ -47,6 +46,7 @@ import com.hedvig.android.logger.logcat import com.hedvig.android.navigation.activity.ExternalNavigator import com.hedvig.android.navigation.compose.typedHasRoute import com.hedvig.android.navigation.core.HedvigDeepLinkContainer +import com.hedvig.android.notification.badge.data.payment.MissedPaymentNotificationServiceProvider import com.hedvig.android.ui.force.upgrade.ForceUpgradeBlockingScreen import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigAppState.kt b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigAppState.kt index e0d8dba515..390d3efddb 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigAppState.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigAppState.kt @@ -178,7 +178,6 @@ internal class HedvigAppState( false, ) - /** * UI logic for navigating to a top level destination in the app. Top level destinations have * only one copy of the destination of the back stack, and save and restore state whenever you @@ -222,7 +221,6 @@ internal class HedvigAppState( } fun navigateToLoggedOut() { - val isLoggingOutFromProfile = navController.currentDestination ?.typedHasRoute(destinationToExcludeFromSavingState) ?: false diff --git a/app/compose/compose-ui/src/commonMain/kotlin/com/hedvig/android/compose/ui/StickyHeaderTopInset.kt b/app/compose/compose-ui/src/commonMain/kotlin/com/hedvig/android/compose/ui/StickyHeaderTopInset.kt new file mode 100644 index 0000000000..ee97bd18fa --- /dev/null +++ b/app/compose/compose-ui/src/commonMain/kotlin/com/hedvig/android/compose/ui/StickyHeaderTopInset.kt @@ -0,0 +1,41 @@ +package com.hedvig.android.compose.ui + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp + +/** + * For a LazyColumn whose content scrolls behind a translucent system bar (so its + * `contentPadding.top` is the bar's inset), a `stickyHeader` will pin against the screen + * edge and end up behind the bar. Apply the returned Dp as `Modifier.padding(top = ...)` on + * the sticky's inner content to keep it visible just below the bar instead. + * + * ``` + * stickyHeader(key = HeaderKey) { + * val top = rememberStickyHeaderTopInset(listState, HeaderKey, contentPadding.calculateTopPadding()) + * Surface(modifier = Modifier.fillMaxWidth()) { + * Column(modifier = Modifier.padding(top = top)) { /* header content */ } + * } + * } + * ``` + * + * Returns `0.dp` when [topContentPadding] is `0.dp`, so it's a no-op on screens where the + * top bar is drawn by Compose (Android) and only does work on screens where the list scrolls + * behind the top bar. + */ +@Composable +fun rememberStickyHeaderTopInset(listState: LazyListState, stickyHeaderKey: Any, topContentPadding: Dp): Dp { + val density = LocalDensity.current + val topContentPaddingPx = with(density) { topContentPadding.roundToPx() } + val offsetPx by remember(listState, stickyHeaderKey, topContentPaddingPx) { + derivedStateOf { + val info = listState.layoutInfo.visibleItemsInfo.firstOrNull { it.key == stickyHeaderKey } + if (info == null) 0 else (-info.offset).coerceIn(0, topContentPaddingPx) + } + } + return with(density) { offsetPx.toDp() } +} diff --git a/app/core/core-datastore-public/build.gradle.kts b/app/core/core-datastore-public/build.gradle.kts index 2d541e69f2..151e4539c0 100644 --- a/app/core/core-datastore-public/build.gradle.kts +++ b/app/core/core-datastore-public/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("hedvig.multiplatform.library") id("hedvig.multiplatform.library.android") id("hedvig.gradle.plugin") + alias(libs.plugins.kmpNativeCoroutines) } kotlin { diff --git a/app/core/core-resources/build.gradle.kts b/app/core/core-resources/build.gradle.kts index 89668fcfeb..23f57b1ae5 100644 --- a/app/core/core-resources/build.gradle.kts +++ b/app/core/core-resources/build.gradle.kts @@ -33,7 +33,6 @@ lokalise { kotlin { androidLibrary { namespace = "hedvig.resources" - @Suppress("UnstableApiUsage") androidResources.enable = true } sourceSets { 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 16c7b47bd7..04fbb46116 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 @@ -15,6 +15,7 @@ av Pausa Spela + Läst Skrolla längst ner på sidan Inskickat värde @@ -998,6 +999,7 @@ Vi kan tyvärr inte ge dig ett pris. Var god se till att du har Kivras digitala brevlåda och försök igen.\n\nDu kan skaffa digital brevlåda hos Kivra här: https://kivra.se/sv/privat/sa-funkar-det/digitala-brev-brevlada Rabattnamn: %s ändra värde + Bild Tryck för mer information Kampanjikon Informationsikon 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 d7349d1b0b..d25b79a81f 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -15,6 +15,7 @@ of Pause Play + Red Scroll to bottom Submitted value @@ -998,6 +999,7 @@ Sorry, we couldn’t generate a price. Please make sure you have Kivra\'s digital mailbox and then try again. \n\nYou can get Kivra\'s digital mailbox here: https://kivra.se/en/private/how-it-works/digital-letters-mailbox. Discount name: %s change value + Image Tap for more info Campaign image Information image diff --git a/app/core/core-resources/src/commonMain/composeResources/drawable/hundar_badar_pet.jpg b/app/core/core-resources/src/commonMain/composeResources/drawable/hundar_badar_pet.jpg new file mode 100644 index 0000000000..c7e8f8bfc2 Binary files /dev/null and b/app/core/core-resources/src/commonMain/composeResources/drawable/hundar_badar_pet.jpg differ diff --git a/app/core/core-resources/src/commonMain/composeResources/drawable/pillow_hedvig.png b/app/core/core-resources/src/commonMain/composeResources/drawable/pillow_hedvig.png new file mode 100644 index 0000000000..7e9a452ab9 Binary files /dev/null and b/app/core/core-resources/src/commonMain/composeResources/drawable/pillow_hedvig.png differ 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 7e3a42db9f..f541fcefc3 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 @@ -1,4 +1,3 @@ - Svenska English @@ -15,6 +14,7 @@ av Pausa Spela + Läst Skrolla längst ner på sidan Inskickat värde @@ -998,6 +998,7 @@ Vi kan tyvärr inte ge dig ett pris. Var god se till att du har Kivras digitala brevlåda och försök igen.\n\nDu kan skaffa digital brevlåda hos Kivra här: https://kivra.se/sv/privat/sa-funkar-det/digitala-brev-brevlada Rabattnamn: %1$s ändra värde + Bild Tryck för mer information Kampanjikon Informationsikon 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 fe2c250455..18bdcd8887 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -1,4 +1,3 @@ - Svenska English @@ -15,6 +14,7 @@ of Pause Play + Red Scroll to bottom Submitted value @@ -998,6 +998,7 @@ Sorry, we couldn’t generate a price. Please make sure you have Kivra's digital mailbox and then try again. \n\nYou can get Kivra's digital mailbox here: https://kivra.se/en/private/how-it-works/digital-letters-mailbox. Discount name: %1$s change value + Image Tap for more info Campaign image Information image diff --git a/app/data/data-addons/build.gradle.kts b/app/data/data-addons/build.gradle.kts index e2180dcc20..8b0ec31e99 100644 --- a/app/data/data-addons/build.gradle.kts +++ b/app/data/data-addons/build.gradle.kts @@ -22,7 +22,7 @@ dependencies { implementation(projects.apolloOctopusPublic) implementation(projects.coreCommonPublic) implementation(projects.coreDemoMode) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) testImplementation(libs.apollo.testingSupport) testImplementation(libs.assertK) diff --git a/app/data/data-changetier/build.gradle.kts b/app/data/data-changetier/build.gradle.kts index 0ee48dd9db..2b365576ab 100644 --- a/app/data/data-changetier/build.gradle.kts +++ b/app/data/data-changetier/build.gradle.kts @@ -25,7 +25,7 @@ dependencies { implementation(projects.dataContract) implementation(projects.dataCrossSellAfterFlow) implementation(projects.dataProductVariantPublic) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.uiTiersAndAddons) testImplementation(libs.apollo.testingSupport) diff --git a/app/data/data-conversations/build.gradle.kts b/app/data/data-conversations/build.gradle.kts index f24062dd2f..71ad02e8ed 100644 --- a/app/data/data-conversations/build.gradle.kts +++ b/app/data/data-conversations/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("hedvig.jvm.library") + id("hedvig.multiplatform.library") id("hedvig.gradle.plugin") } @@ -7,10 +7,15 @@ hedvig { apollo("octopus") } -dependencies { - implementation(libs.apollo.normalizedCache) - implementation(libs.arrow.core) - implementation(libs.koin.core) - implementation(projects.apolloCore) - implementation(projects.apolloOctopusPublic) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.apollo.normalizedCache) + implementation(libs.arrow.core) + implementation(libs.koin.core) + implementation(projects.apolloCore) + implementation(projects.apolloOctopusPublic) + implementation(projects.loggingPublic) + } + } } diff --git a/app/data/data-conversations/src/main/graphql/QueryCbmNumberOfChatMessages.graphql b/app/data/data-conversations/src/commonMain/graphql/QueryCbmNumberOfChatMessages.graphql similarity index 100% rename from app/data/data-conversations/src/main/graphql/QueryCbmNumberOfChatMessages.graphql rename to app/data/data-conversations/src/commonMain/graphql/QueryCbmNumberOfChatMessages.graphql diff --git a/app/data/data-conversations/src/main/kotlin/com/hedvig/android/data/conversations/HasAnyActiveConversationUseCase.kt b/app/data/data-conversations/src/commonMain/kotlin/com/hedvig/android/data/conversations/HasAnyActiveConversationUseCase.kt similarity index 100% rename from app/data/data-conversations/src/main/kotlin/com/hedvig/android/data/conversations/HasAnyActiveConversationUseCase.kt rename to app/data/data-conversations/src/commonMain/kotlin/com/hedvig/android/data/conversations/HasAnyActiveConversationUseCase.kt diff --git a/app/data/data-conversations/src/main/kotlin/com/hedvig/android/data/conversations/di/DataConversationsModule.kt b/app/data/data-conversations/src/commonMain/kotlin/com/hedvig/android/data/conversations/di/DataConversationsModule.kt similarity index 100% rename from app/data/data-conversations/src/main/kotlin/com/hedvig/android/data/conversations/di/DataConversationsModule.kt rename to app/data/data-conversations/src/commonMain/kotlin/com/hedvig/android/data/conversations/di/DataConversationsModule.kt diff --git a/app/data/data-termination/build.gradle.kts b/app/data/data-termination/build.gradle.kts index e7d7fe9875..8059e0acef 100644 --- a/app/data/data-termination/build.gradle.kts +++ b/app/data/data-termination/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("hedvig.jvm.library") + id("hedvig.multiplatform.library") id("hedvig.gradle.plugin") } @@ -7,16 +7,20 @@ hedvig { apollo("octopus") } -dependencies { - implementation(libs.apollo.normalizedCache) - implementation(libs.apollo.runtime) - implementation(libs.arrow.core) - implementation(libs.koin.core) - implementation(libs.kotlinx.datetime) - implementation(projects.apolloCore) - implementation(projects.apolloOctopusPublic) - implementation(projects.coreCommonPublic) - implementation(projects.coreUiData) - implementation(projects.dataContract) - implementation(projects.featureFlagsPublic) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.apollo.normalizedCache) + implementation(libs.apollo.runtime) + implementation(libs.arrow.core) + implementation(libs.koin.core) + implementation(libs.kotlinx.datetime) + implementation(projects.apolloCore) + implementation(projects.apolloOctopusPublic) + implementation(projects.coreCommonPublic) + implementation(projects.coreUiData) + implementation(projects.dataContract) + implementation(projects.featureFlags) + } + } } diff --git a/app/data/data-termination/src/main/graphql/QueryContractsToTerminate.graphql b/app/data/data-termination/src/commonMain/graphql/QueryContractsToTerminate.graphql similarity index 100% rename from app/data/data-termination/src/main/graphql/QueryContractsToTerminate.graphql rename to app/data/data-termination/src/commonMain/graphql/QueryContractsToTerminate.graphql diff --git a/app/data/data-termination/src/main/kotlin/com/hedvig/android/data/termination/data/GetTerminatableContractsUseCase.kt b/app/data/data-termination/src/commonMain/kotlin/com/hedvig/android/data/termination/data/GetTerminatableContractsUseCase.kt similarity index 100% rename from app/data/data-termination/src/main/kotlin/com/hedvig/android/data/termination/data/GetTerminatableContractsUseCase.kt rename to app/data/data-termination/src/commonMain/kotlin/com/hedvig/android/data/termination/data/GetTerminatableContractsUseCase.kt diff --git a/app/data/data-termination/src/main/kotlin/com/hedvig/android/data/termination/di/TerminationDataModule.kt b/app/data/data-termination/src/commonMain/kotlin/com/hedvig/android/data/termination/di/TerminationDataModule.kt similarity index 100% rename from app/data/data-termination/src/main/kotlin/com/hedvig/android/data/termination/di/TerminationDataModule.kt rename to app/data/data-termination/src/commonMain/kotlin/com/hedvig/android/data/termination/di/TerminationDataModule.kt diff --git a/app/datadog/datadog-core/build.gradle.kts b/app/datadog/datadog-core/build.gradle.kts index 95dccf96b9..881a12e9e9 100644 --- a/app/datadog/datadog-core/build.gradle.kts +++ b/app/datadog/datadog-core/build.gradle.kts @@ -1,8 +1,12 @@ plugins { - id("hedvig.jvm.library") + id("hedvig.multiplatform.library") id("hedvig.gradle.plugin") } -dependencies { - implementation(libs.coroutines.core) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.coroutines.core) + } + } } diff --git a/app/datadog/datadog-core/src/main/kotlin/com/hedvig/android/datadog/core/attributestracking/DatadogAttributeProvider.kt b/app/datadog/datadog-core/src/commonMain/kotlin/com/hedvig/android/datadog/core/attributestracking/DatadogAttributeProvider.kt similarity index 100% rename from app/datadog/datadog-core/src/main/kotlin/com/hedvig/android/datadog/core/attributestracking/DatadogAttributeProvider.kt rename to app/datadog/datadog-core/src/commonMain/kotlin/com/hedvig/android/datadog/core/attributestracking/DatadogAttributeProvider.kt diff --git a/app/datadog/datadog-core/src/main/kotlin/com/hedvig/android/datadog/core/attributestracking/DatadogMemberIdProvider.kt b/app/datadog/datadog-core/src/commonMain/kotlin/com/hedvig/android/datadog/core/attributestracking/DatadogMemberIdProvider.kt similarity index 100% rename from app/datadog/datadog-core/src/main/kotlin/com/hedvig/android/datadog/core/attributestracking/DatadogMemberIdProvider.kt rename to app/datadog/datadog-core/src/commonMain/kotlin/com/hedvig/android/datadog/core/attributestracking/DatadogMemberIdProvider.kt diff --git a/app/design-system/design-system-api/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/api/IosSwipeBackController.kt b/app/design-system/design-system-api/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/api/IosSwipeBackController.kt new file mode 100644 index 0000000000..bb130255c9 --- /dev/null +++ b/app/design-system/design-system-api/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/api/IosSwipeBackController.kt @@ -0,0 +1,10 @@ +package com.hedvig.android.design.system.hedvig.api + +/** + * Implemented by Swift. While a pointer is down on a marked region we + * disable the system swipe-back so Compose's horizontal scroll can win; + * on touch end we re-enable it. + */ +interface IosSwipeBackController { + fun setSwipeBackEnabled(isEnabled: Boolean) +} diff --git a/app/design-system/design-system-hedvig/build.gradle.kts b/app/design-system/design-system-hedvig/build.gradle.kts index a974b9fe04..41ddca940c 100644 --- a/app/design-system/design-system-hedvig/build.gradle.kts +++ b/app/design-system/design-system-hedvig/build.gradle.kts @@ -45,18 +45,13 @@ kotlin { implementation(libs.jetbrains.lifecycle.runtime.compose) implementation(libs.jetbrains.navigationevent.compose) implementation(libs.kotlinx.datetime) + implementation(libs.mikepenz.markdown) implementation(projects.composeUi) implementation(projects.coreResources) implementation(projects.coreUiData) implementation(projects.designSystemInternals) implementation(projects.navigationCore) } - val jvmAndAndroidMain by getting { - dependencies { - implementation(libs.compose.richtext) - implementation(libs.compose.richtextCommonmark) - } - } androidMain.dependencies { implementation(libs.androidx.other.core) implementation(libs.media3.exoplayer) diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/EmptyState.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/EmptyState.kt index 567de7b4fd..ffa183ce3b 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/EmptyState.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/EmptyState.kt @@ -161,7 +161,7 @@ object EmptyStateDefaults { SUCCESS, BANK_ID, NO_ICON, - SUCCESS_WITH_WARNING + SUCCESS_WITH_WARNING, } sealed class EmptyStateButtonStyle { diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HighlightLabel.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HighlightLabel.kt index ab17a24f19..850efa2035 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HighlightLabel.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HighlightLabel.kt @@ -1,6 +1,7 @@ package com.hedvig.android.design.system.hedvig import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable @@ -30,6 +31,23 @@ import com.hedvig.android.design.system.hedvig.tokens.HighlightLabelTokens @Composable fun HighlightLabel(labelText: String, size: HighLightSize, color: HighlightColor, modifier: Modifier = Modifier) { + HighlightLabel(size, color, modifier) { + HedvigText( + text = labelText, + textAlign = TextAlign.Center, + style = size.textStyle, + modifier = Modifier.padding(size.contentPadding), + ) + } +} + +@Composable +fun HighlightLabel( + size: HighLightSize, + color: HighlightColor, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { val surfaceColor: Color = when (color) { is Amber -> { when (color.shade) { @@ -127,7 +145,7 @@ fun HighlightLabel(labelText: String, size: HighLightSize, color: HighlightColor highLightColors.outlineContainer } } - val textColor = when (color) { + val contentColor = when (color) { is Grey -> { when (color.shade) { LIGHT -> highLightColors.greyLightTextColor @@ -156,15 +174,10 @@ fun HighlightLabel(labelText: String, size: HighLightSize, color: HighlightColor modifier = modifier, shape = size.shape, color = surfaceColor, + contentColor = contentColor, border = borderColor, ) { - HedvigText( - text = labelText, - textAlign = TextAlign.Center, - style = size.textStyle, - color = textColor, - modifier = Modifier.padding(size.contentPadding), - ) + content() } } diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/InteractiveComponentSize.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/InteractiveComponentSize.kt index d15b8e7ef4..9577ebd853 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/InteractiveComponentSize.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/InteractiveComponentSize.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.isSpecified +import com.hedvig.android.design.system.hedvig.internal.identityHashCode import kotlin.math.roundToInt /** @@ -36,7 +37,7 @@ internal object MinimumInteractiveModifier : ModifierNodeElement AnnotatedString.Builder.withHedvigLink( + tag: String, + noinline onClick: () -> Unit, + block: AnnotatedString.Builder.() -> R, +): R { + return withLink( + LinkAnnotation.Clickable( + tag = tag, + linkInteractionListener = { onClick() }, + styles = TextLinkStyles( + SpanStyle( + textDecoration = TextDecoration.Underline, + color = HedvigTheme.colorScheme.link, + ), + ), + ), + ) { + block() + } +} + +@Composable +inline fun AnnotatedString.Builder.withHedvigLink(url: String, block: AnnotatedString.Builder.() -> R): R { + return withLink( + LinkAnnotation.Url( + url = url, + styles = TextLinkStyles( + SpanStyle( + textDecoration = TextDecoration.Underline, + color = HedvigTheme.colorScheme.link, + ), + ), + ), + ) { + block() + } +} diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Ripple.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Ripple.kt index eee264b7dd..7f9e38e0d1 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Ripple.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Ripple.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.Stable import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorProducer import androidx.compose.ui.graphics.isSpecified @@ -19,6 +20,7 @@ import androidx.compose.ui.node.ObserverModifierNode import androidx.compose.ui.node.currentValueOf import androidx.compose.ui.node.observeReads import androidx.compose.ui.unit.Dp +import com.hedvig.android.design.system.hedvig.internal.identityHashCode import com.hedvig.android.design.system.hedvig.tokens.StateTokens /** @@ -31,18 +33,28 @@ fun ripple( radius: Dp = Dp.Unspecified, color: Color = Color.Unspecified, ): IndicationNodeFactory { + if (rippleIsNoopOnThisPlatform) return NoopIndication return if (radius == Dp.Unspecified && color == Color.Unspecified) { - if (bounded) return DefaultBoundedRipple else DefaultUnboundedRipple + if (bounded) DefaultBoundedRipple else DefaultUnboundedRipple } else { RippleNodeFactory(bounded, radius, color) } } -fun noopRipple(): Indication = NoopIndication +fun noopRipple(): IndicationNodeFactory = NoopIndication + +/** + * iOS has no ripple — UIKit doesn't show one and Compose's ripple looks foreign there. + * Routed through [ripple] (and the internal [ColorProducer] overload) so every code path + * — `LocalIndication.current`, direct `ripple()`, custom-color `ripple(...)` — collapses to + * [NoopIndication] on iOS without callers having to know. + */ +internal expect val rippleIsNoopOnThisPlatform: Boolean @Suppress("unused") @Stable internal fun ripple(color: ColorProducer, bounded: Boolean = true, radius: Dp = Dp.Unspecified): IndicationNodeFactory { + if (rippleIsNoopOnThisPlatform) return NoopIndication return RippleNodeFactory(bounded, radius, color) } @@ -189,8 +201,12 @@ private val DefaultUnboundedRipple = RippleNodeFactory( color = Color.Unspecified, ) -private object NoopIndication : Indication { +private object NoopIndication : IndicationNodeFactory { + override fun create(interactionSource: InteractionSource): DelegatableNode = NoopIndicationNode() + override fun equals(other: Any?): Boolean = other === NoopIndication - override fun hashCode(): Int = System.identityHashCode(this) + override fun hashCode(): Int = identityHashCode(this) } + +private class NoopIndicationNode : Modifier.Node() diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/blockSwipeBackOnIos.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/blockSwipeBackOnIos.kt new file mode 100644 index 0000000000..4f350a54a3 --- /dev/null +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/blockSwipeBackOnIos.kt @@ -0,0 +1,7 @@ +package com.hedvig.android.design.system.hedvig + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +expect fun Modifier.blockSwipeBackOnIos(): Modifier diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/ArrowLeft.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/ArrowLeft.kt index ebc0e7506f..dc0f8243f1 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/ArrowLeft.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/ArrowLeft.kt @@ -1,141 +1,5 @@ package com.hedvig.android.design.system.hedvig.icon -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.PathFillType -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.path -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.hedvig.android.compose.ui.EmptyContentDescription -import com.hedvig.android.design.system.hedvig.HedvigTheme -@Suppress("UnusedReceiverParameter") -val HedvigIcons.ArrowLeft: ImageVector - get() { - val current = _arrowLeft - if (current != null) return current - - return ImageVector.Builder( - name = "com.hedvig.android.design.system.hedvig.icon.ArrowLeft", - defaultWidth = 24.0.dp, - defaultHeight = 24.0.dp, - viewportWidth = 24.0f, - viewportHeight = 24.0f, - ).apply { - // M12.5303 6.03033 C12.8232 5.73744 12.8232 5.26256 12.5303 4.96967 C12.2374 4.67678 11.7626 4.67678 11.4697 4.96967 L5.67678 10.7626 C4.99336 11.446 4.99336 12.554 5.67678 13.2374 L11.4697 19.0303 C11.7626 19.3232 12.2374 19.3232 12.5303 19.0303 C12.8232 18.7374 12.8232 18.2626 12.5303 17.9697 L7.31066 12.75 H19 C19.4142 12.75 19.75 12.4142 19.75 12 C19.75 11.5858 19.4142 11.25 19 11.25 H7.31066 L12.5303 6.03033Z - path( - fill = SolidColor(Color(0xFF121212)), - pathFillType = PathFillType.EvenOdd, - ) { - // M 12.5303 6.03033 - moveTo(x = 12.5303f, y = 6.03033f) - // C 12.8232 5.73744 12.8232 5.26256 12.5303 4.96967 - curveTo( - x1 = 12.8232f, - y1 = 5.73744f, - x2 = 12.8232f, - y2 = 5.26256f, - x3 = 12.5303f, - y3 = 4.96967f, - ) - // C 12.2374 4.67678 11.7626 4.67678 11.4697 4.96967 - curveTo( - x1 = 12.2374f, - y1 = 4.67678f, - x2 = 11.7626f, - y2 = 4.67678f, - x3 = 11.4697f, - y3 = 4.96967f, - ) - // L 5.67678 10.7626 - lineTo(x = 5.67678f, y = 10.7626f) - // C 4.99336 11.446 4.99336 12.554 5.67678 13.2374 - curveTo( - x1 = 4.99336f, - y1 = 11.446f, - x2 = 4.99336f, - y2 = 12.554f, - x3 = 5.67678f, - y3 = 13.2374f, - ) - // L 11.4697 19.0303 - lineTo(x = 11.4697f, y = 19.0303f) - // C 11.7626 19.3232 12.2374 19.3232 12.5303 19.0303 - curveTo( - x1 = 11.7626f, - y1 = 19.3232f, - x2 = 12.2374f, - y2 = 19.3232f, - x3 = 12.5303f, - y3 = 19.0303f, - ) - // C 12.8232 18.7374 12.8232 18.2626 12.5303 17.9697 - curveTo( - x1 = 12.8232f, - y1 = 18.7374f, - x2 = 12.8232f, - y2 = 18.2626f, - x3 = 12.5303f, - y3 = 17.9697f, - ) - // L 7.31066 12.75 - lineTo(x = 7.31066f, y = 12.75f) - // H 19 - horizontalLineTo(x = 19.0f) - // C 19.4142 12.75 19.75 12.4142 19.75 12 - curveTo( - x1 = 19.4142f, - y1 = 12.75f, - x2 = 19.75f, - y2 = 12.4142f, - x3 = 19.75f, - y3 = 12.0f, - ) - // C 19.75 11.5858 19.4142 11.25 19 11.25 - curveTo( - x1 = 19.75f, - y1 = 11.5858f, - x2 = 19.4142f, - y2 = 11.25f, - x3 = 19.0f, - y3 = 11.25f, - ) - // H 7.31066 - horizontalLineTo(x = 7.31066f) - // L 12.5303 6.03033z - lineTo(x = 12.5303f, y = 6.03033f) - close() - } - }.build().also { _arrowLeft = it } - } - -@Preview -@Composable -private fun IconPreview() { - HedvigTheme { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image( - imageVector = HedvigIcons.ArrowLeft, - contentDescription = EmptyContentDescription, - modifier = Modifier - .width((24.0).dp) - .height((24.0).dp), - ) - } - } -} - -@Suppress("ObjectPropertyName", "ktlint:standard:backing-property-naming") -private var _arrowLeft: ImageVector? = null +expect val HedvigIcons.ArrowLeft: ImageVector diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/Bookmark.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/Bookmark.kt index 167dac6413..06707bf995 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/Bookmark.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/Bookmark.kt @@ -32,7 +32,7 @@ val HedvigIcons.Bookmark: ImageVector ).apply { path( fill = SolidColor(Color(0xFF121212)), - pathFillType = PathFillType.EvenOdd + pathFillType = PathFillType.EvenOdd, ) { moveTo(7.5f, 3.75f) curveTo(6.80964f, 3.75f, 6.25f, 4.30964f, 6.25f, 5f) diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/internal/System.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/internal/System.kt new file mode 100644 index 0000000000..b00a4e0939 --- /dev/null +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/internal/System.kt @@ -0,0 +1,3 @@ +package com.hedvig.android.design.system.hedvig.internal + +internal expect fun identityHashCode(instance: Any?): Int diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/placeholder/Placeholder.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/placeholder/Placeholder.kt index d898d067f3..76ce52249e 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/placeholder/Placeholder.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/placeholder/Placeholder.kt @@ -11,6 +11,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigTheme @@ -60,14 +62,16 @@ fun Modifier.hedvigPlaceholder( contentFadeAnimationSpec: AnimationSpec = spring(), ): Modifier = this.composed { val themedColor = if (color.isSpecified) color else PlaceholderDefaults.color() - Modifier.corePlaceholder( - visible = visible, - color = themedColor, - shape = shape, - highlight = highlight, - placeholderFadeAnimationSpec = placeholderFadeAnimationSpec, - contentFadeAnimationSpec = contentFadeAnimationSpec, - ) + Modifier + .semantics { hideFromAccessibility() } + .corePlaceholder( + visible = visible, + color = themedColor, + shape = shape, + highlight = highlight, + placeholderFadeAnimationSpec = placeholderFadeAnimationSpec, + contentFadeAnimationSpec = contentFadeAnimationSpec, + ) } @HedvigPreview diff --git a/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/RichText.kt b/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/RichText.kt deleted file mode 100644 index 1a31361a68..0000000000 --- a/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/RichText.kt +++ /dev/null @@ -1,124 +0,0 @@ -package com.hedvig.android.design.system.hedvig - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.LinkAnnotation -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextLinkStyles -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.withLink -import com.halilibo.richtext.ui.BasicRichText -import com.halilibo.richtext.ui.RichTextScope -import com.halilibo.richtext.ui.RichTextStyle -import com.halilibo.richtext.ui.RichTextThemeProvider -import com.halilibo.richtext.ui.merge -import com.halilibo.richtext.ui.string.RichTextStringStyle - -/** - * RichText implementation that integrates with HedvigTheme. - * - */ -@Composable -fun RichText( - modifier: Modifier = Modifier, - style: RichTextStyle? = null, - children: @Composable RichTextScope.() -> Unit, -) { - val linkColor = HedvigTheme.colorScheme.link - val linkStyle = RichTextStyle.Default.copy( - stringStyle = RichTextStringStyle( - linkStyle = TextLinkStyles( - SpanStyle( - textDecoration = TextDecoration.Underline, - color = linkColor, - ), - ), - ), - ) - RichTextHedvigTheme { - BasicRichText( - modifier = modifier, - style = linkStyle.merge(style), - children = children, - ) - } -} - -/** - * [tag] is also used to name the a11y action, so it must be an appropriate user-facing copy - */ -@Composable -inline fun AnnotatedString.Builder.withHedvigLink( - tag: String, - noinline onClick: () -> Unit, - block: AnnotatedString.Builder.() -> R, -): R { - return withLink( - LinkAnnotation.Clickable( - tag = tag, - linkInteractionListener = { onClick() }, - styles = TextLinkStyles( - SpanStyle( - textDecoration = TextDecoration.Underline, - color = HedvigTheme.colorScheme.link, - ), - ), - ), - ) { - block() - } -} - -@Composable -inline fun AnnotatedString.Builder.withHedvigLink(url: String, block: AnnotatedString.Builder.() -> R): R { - return withLink( - LinkAnnotation.Url( - url = url, - styles = TextLinkStyles( - SpanStyle( - textDecoration = TextDecoration.Underline, - color = HedvigTheme.colorScheme.link, - ), - ), - ), - ) { - block() - } -} - -/** - * Wraps the given [child] with Hedvig Theme integration for [com.halilibo.richtext.ui.BasicRichText]. - * - * This function also keeps track of the parent context by using CompositionLocals - * to not apply Material Theming if it already exists in the current composition. - */ -@Composable -private fun RichTextHedvigTheme(child: @Composable () -> Unit) { - val isApplied = LocalThemingApplied.current - - if (!isApplied) { - RichTextThemeProvider( - textStyleProvider = { LocalTextStyle.current }, - contentColorProvider = { LocalContentColor.current }, - textStyleBackProvider = { textStyle, content -> - ProvideTextStyle(textStyle, content) - }, - contentColorBackProvider = { color, content -> - CompositionLocalProvider(LocalContentColor provides color) { - content() - } - }, - ) { - CompositionLocalProvider(LocalThemingApplied provides true) { - child() - } - } - } else { - child() - } -} - -private val LocalThemingApplied = compositionLocalOf { false } diff --git a/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/Ripple.jvmAndAndroid.kt b/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/Ripple.jvmAndAndroid.kt new file mode 100644 index 0000000000..23fcd2da20 --- /dev/null +++ b/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/Ripple.jvmAndAndroid.kt @@ -0,0 +1,3 @@ +package com.hedvig.android.design.system.hedvig + +internal actual val rippleIsNoopOnThisPlatform: Boolean = false diff --git a/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/blockSwipeBackOnIos.jvmAndAndroid.kt b/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/blockSwipeBackOnIos.jvmAndAndroid.kt new file mode 100644 index 0000000000..0d4e57ecdd --- /dev/null +++ b/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/blockSwipeBackOnIos.jvmAndAndroid.kt @@ -0,0 +1,7 @@ +package com.hedvig.android.design.system.hedvig + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +actual fun Modifier.blockSwipeBackOnIos(): Modifier = this diff --git a/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/icon/ArrowLeft.jvmAndAndroid.kt b/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/icon/ArrowLeft.jvmAndAndroid.kt new file mode 100644 index 0000000000..632c7f0fc9 --- /dev/null +++ b/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/icon/ArrowLeft.jvmAndAndroid.kt @@ -0,0 +1,141 @@ +package com.hedvig.android.design.system.hedvig.icon + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.hedvig.android.compose.ui.EmptyContentDescription +import com.hedvig.android.design.system.hedvig.HedvigTheme + +@Suppress("UnusedReceiverParameter") +actual val HedvigIcons.ArrowLeft: ImageVector + get() { + val current = _arrowLeft + if (current != null) return current + + return ImageVector.Builder( + name = "com.hedvig.android.design.system.hedvig.icon.ArrowLeft", + defaultWidth = 24.0.dp, + defaultHeight = 24.0.dp, + viewportWidth = 24.0f, + viewportHeight = 24.0f, + ).apply { + // M12.5303 6.03033 C12.8232 5.73744 12.8232 5.26256 12.5303 4.96967 C12.2374 4.67678 11.7626 4.67678 11.4697 4.96967 L5.67678 10.7626 C4.99336 11.446 4.99336 12.554 5.67678 13.2374 L11.4697 19.0303 C11.7626 19.3232 12.2374 19.3232 12.5303 19.0303 C12.8232 18.7374 12.8232 18.2626 12.5303 17.9697 L7.31066 12.75 H19 C19.4142 12.75 19.75 12.4142 19.75 12 C19.75 11.5858 19.4142 11.25 19 11.25 H7.31066 L12.5303 6.03033Z + path( + fill = SolidColor(Color(0xFF121212)), + pathFillType = PathFillType.EvenOdd, + ) { + // M 12.5303 6.03033 + moveTo(x = 12.5303f, y = 6.03033f) + // C 12.8232 5.73744 12.8232 5.26256 12.5303 4.96967 + curveTo( + x1 = 12.8232f, + y1 = 5.73744f, + x2 = 12.8232f, + y2 = 5.26256f, + x3 = 12.5303f, + y3 = 4.96967f, + ) + // C 12.2374 4.67678 11.7626 4.67678 11.4697 4.96967 + curveTo( + x1 = 12.2374f, + y1 = 4.67678f, + x2 = 11.7626f, + y2 = 4.67678f, + x3 = 11.4697f, + y3 = 4.96967f, + ) + // L 5.67678 10.7626 + lineTo(x = 5.67678f, y = 10.7626f) + // C 4.99336 11.446 4.99336 12.554 5.67678 13.2374 + curveTo( + x1 = 4.99336f, + y1 = 11.446f, + x2 = 4.99336f, + y2 = 12.554f, + x3 = 5.67678f, + y3 = 13.2374f, + ) + // L 11.4697 19.0303 + lineTo(x = 11.4697f, y = 19.0303f) + // C 11.7626 19.3232 12.2374 19.3232 12.5303 19.0303 + curveTo( + x1 = 11.7626f, + y1 = 19.3232f, + x2 = 12.2374f, + y2 = 19.3232f, + x3 = 12.5303f, + y3 = 19.0303f, + ) + // C 12.8232 18.7374 12.8232 18.2626 12.5303 17.9697 + curveTo( + x1 = 12.8232f, + y1 = 18.7374f, + x2 = 12.8232f, + y2 = 18.2626f, + x3 = 12.5303f, + y3 = 17.9697f, + ) + // L 7.31066 12.75 + lineTo(x = 7.31066f, y = 12.75f) + // H 19 + horizontalLineTo(x = 19.0f) + // C 19.4142 12.75 19.75 12.4142 19.75 12 + curveTo( + x1 = 19.4142f, + y1 = 12.75f, + x2 = 19.75f, + y2 = 12.4142f, + x3 = 19.75f, + y3 = 12.0f, + ) + // C 19.75 11.5858 19.4142 11.25 19 11.25 + curveTo( + x1 = 19.75f, + y1 = 11.5858f, + x2 = 19.4142f, + y2 = 11.25f, + x3 = 19.0f, + y3 = 11.25f, + ) + // H 7.31066 + horizontalLineTo(x = 7.31066f) + // L 12.5303 6.03033z + lineTo(x = 12.5303f, y = 6.03033f) + close() + } + }.build().also { _arrowLeft = it } + } + +@Preview +@Composable +private fun IconPreview() { + HedvigTheme { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + imageVector = HedvigIcons.ArrowLeft, + contentDescription = EmptyContentDescription, + modifier = Modifier + .width((24.0).dp) + .height((24.0).dp), + ) + } + } +} + +@Suppress("ObjectPropertyName", "ktlint:standard:backing-property-naming") +private var _arrowLeft: ImageVector? = null diff --git a/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/internal/System.jvmAndAndroid.kt b/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/internal/System.jvmAndAndroid.kt new file mode 100644 index 0000000000..c47098df7b --- /dev/null +++ b/app/design-system/design-system-hedvig/src/jvmAndAndroidMain/kotlin/com/hedvig/android/design/system/hedvig/internal/System.jvmAndAndroid.kt @@ -0,0 +1,3 @@ +package com.hedvig.android.design.system.hedvig.internal + +internal actual fun identityHashCode(instance: Any?): Int = System.identityHashCode(instance) diff --git a/app/design-system/design-system-hedvig/src/nativeMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigComposeUIViewController.kt b/app/design-system/design-system-hedvig/src/nativeMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigComposeUIViewController.kt new file mode 100644 index 0000000000..b5fb321ec7 --- /dev/null +++ b/app/design-system/design-system-hedvig/src/nativeMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigComposeUIViewController.kt @@ -0,0 +1,47 @@ +package com.hedvig.android.design.system.hedvig + +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.window.ComposeUIViewController +import com.hedvig.android.design.system.hedvig.api.IosSwipeBackController +import platform.UIKit.UIViewController + +private val LocalIosSwipeBackController = staticCompositionLocalOf { null } + +@Suppress("FunctionName") +fun HedvigComposeUIViewController( + swipeBackController: IosSwipeBackController, + content: @Composable () -> Unit, +): UIViewController = ComposeUIViewController { + HedvigTheme { + CompositionLocalProvider(LocalIosSwipeBackController provides swipeBackController) { + content() + } + } +} + +@Composable +actual fun Modifier.blockSwipeBackOnIos(): Modifier { + val controller = LocalIosSwipeBackController.current ?: return this + return this.pointerInput(controller) { + awaitPointerEventScope { + while (true) { + awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial) + try { + controller.setSwipeBackEnabled(false) + do { + val event = awaitPointerEvent(PointerEventPass.Initial) + if (event.changes.none { it.pressed }) break + } while (true) + } finally { + controller.setSwipeBackEnabled(true) + } + } + } + } +} diff --git a/app/design-system/design-system-hedvig/src/nativeMain/kotlin/com/hedvig/android/design/system/hedvig/Ripple.native.kt b/app/design-system/design-system-hedvig/src/nativeMain/kotlin/com/hedvig/android/design/system/hedvig/Ripple.native.kt new file mode 100644 index 0000000000..a015713372 --- /dev/null +++ b/app/design-system/design-system-hedvig/src/nativeMain/kotlin/com/hedvig/android/design/system/hedvig/Ripple.native.kt @@ -0,0 +1,3 @@ +package com.hedvig.android.design.system.hedvig + +internal actual val rippleIsNoopOnThisPlatform: Boolean = true diff --git a/app/design-system/design-system-hedvig/src/nativeMain/kotlin/com/hedvig/android/design/system/hedvig/icon/ArrowLeft.native.kt b/app/design-system/design-system-hedvig/src/nativeMain/kotlin/com/hedvig/android/design/system/hedvig/icon/ArrowLeft.native.kt new file mode 100644 index 0000000000..d3e2a20e17 --- /dev/null +++ b/app/design-system/design-system-hedvig/src/nativeMain/kotlin/com/hedvig/android/design/system/hedvig/icon/ArrowLeft.native.kt @@ -0,0 +1,6 @@ +package com.hedvig.android.design.system.hedvig.icon + +import androidx.compose.ui.graphics.vector.ImageVector + +actual val HedvigIcons.ArrowLeft: ImageVector + get() = ChevronLeft diff --git a/app/design-system/design-system-hedvig/src/nativeMain/kotlin/com/hedvig/android/design/system/hedvig/internal/System.native.kt b/app/design-system/design-system-hedvig/src/nativeMain/kotlin/com/hedvig/android/design/system/hedvig/internal/System.native.kt new file mode 100644 index 0000000000..c21351bf6f --- /dev/null +++ b/app/design-system/design-system-hedvig/src/nativeMain/kotlin/com/hedvig/android/design/system/hedvig/internal/System.native.kt @@ -0,0 +1,7 @@ +package com.hedvig.android.design.system.hedvig.internal + +import kotlin.experimental.ExperimentalNativeApi +import kotlin.native.identityHashCode + +@OptIn(ExperimentalNativeApi::class) +internal actual fun identityHashCode(instance: Any?): Int = instance.identityHashCode() diff --git a/app/feature/feature-addon-purchase/build.gradle.kts b/app/feature/feature-addon-purchase/build.gradle.kts index f4e2c6e8c1..b29893a1a5 100644 --- a/app/feature/feature-addon-purchase/build.gradle.kts +++ b/app/feature/feature-addon-purchase/build.gradle.kts @@ -36,7 +36,7 @@ dependencies { implementation(projects.dataCrossSellAfterFlow) implementation(projects.dataProductVariantPublic) implementation(projects.designSystemHedvig) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.languageCore) implementation(projects.moleculePublic) implementation(projects.navigationCommon) diff --git a/app/feature/feature-chat/build.gradle.kts b/app/feature/feature-chat/build.gradle.kts index bd3ff5225f..f902753fd4 100644 --- a/app/feature/feature-chat/build.gradle.kts +++ b/app/feature/feature-chat/build.gradle.kts @@ -21,10 +21,9 @@ dependencies { implementation(libs.arrow.core) implementation(libs.arrow.fx) implementation(libs.coil.compose) - implementation(libs.compose.richtext) - implementation(libs.compose.richtextCommonmark) implementation(libs.coroutines.core) implementation(libs.jetbrains.lifecycle.runtime.compose) + implementation(libs.jetbrains.markdown) implementation(libs.koin.composeViewModel) implementation(libs.koin.core) implementation(libs.kotlinx.datetime) @@ -49,7 +48,7 @@ dependencies { implementation(projects.coreResources) implementation(projects.dataChat) implementation(projects.designSystemHedvig) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.languageCore) implementation(projects.moleculePublic) implementation(projects.navigationActivity) diff --git a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/MarkdownPlainText.kt b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/MarkdownPlainText.kt index 22466fe926..ddd676e4b8 100644 --- a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/MarkdownPlainText.kt +++ b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/MarkdownPlainText.kt @@ -1,47 +1,46 @@ package com.hedvig.android.feature.chat.inbox -import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions -import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser -import com.halilibo.richtext.markdown.node.AstBlockNodeType -import com.halilibo.richtext.markdown.node.AstCode -import com.halilibo.richtext.markdown.node.AstHardLineBreak -import com.halilibo.richtext.markdown.node.AstImage -import com.halilibo.richtext.markdown.node.AstNode -import com.halilibo.richtext.markdown.node.AstSoftLineBreak -import com.halilibo.richtext.markdown.node.AstText +import org.intellij.markdown.IElementType +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.ast.ASTNode +import org.intellij.markdown.ast.getTextInNode +import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor +import org.intellij.markdown.parser.MarkdownParser // The inbox conversation row shows a single ellipsized preview line with no clickable regions, -// so we strip markdown formatting rather than render it. Walking the commonmark AST means any +// so we strip markdown formatting rather than render it. Walking the markdown AST means any // syntax the parser understands collapses to its text content, so future markdown features need // no changes here. -internal fun String.markdownToPlainText(): String = buildString { - appendPlainText(inboxMarkdownParser.parse(this@markdownToPlainText)) +internal fun String.markdownToPlainText(): String { + val flavour = CommonMarkFlavourDescriptor() + val tree = MarkdownParser(flavour).buildMarkdownTreeFromString(this) + return buildString { + appendPlainText(tree, this@markdownToPlainText) + } } -private val inboxMarkdownParser = CommonmarkAstNodeParser(CommonMarkdownParseOptions(autolink = false)) +private fun StringBuilder.appendPlainText(node: ASTNode, src: String) { + val nodeType = node.type -private fun StringBuilder.appendPlainText(node: AstNode) { - val type = node.type - if (type is AstText) { - append(type.literal) - return - } - if (type is AstCode) { - append(type.literal) + // Check if this is a text node by comparing with known IElementType instances + if (nodeType.toString().contains("TEXT") || nodeType.toString().contains("Code")) { + append(node.getTextInNode(src)) return } - if (type === AstSoftLineBreak || type === AstHardLineBreak) { + + // EOL creates a space + if (nodeType.toString().contains("EOL")) { append(' ') return } - // Alt text isn't useful in a one-line preview, so we drop the image entirely. - if (type is AstImage) return - // Container node — recurse into children, separating block-level siblings with a space so - // paragraphs don't run together. - var child = node.links.firstChild - while (child != null) { - appendPlainText(child) - if (child.links.next != null && child.type is AstBlockNodeType) append(' ') - child = child.links.next + + // Drop images + if (nodeType == MarkdownElementTypes.IMAGE) return + + // Recurse into children + node.children.forEach { child -> + appendPlainText(child, src) + // Add space between block elements + if (child.type.toString().contains("Block")) append(' ') } } diff --git a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/ui/ChatBanner.kt b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/ui/ChatBanner.kt index 3e18927c2c..f1655d459e 100644 --- a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/ui/ChatBanner.kt +++ b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/ui/ChatBanner.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewFontScale -import com.halilibo.richtext.commonmark.Markdown +import com.hedvig.android.design.system.hedvig.HedvigMarkdownText import com.hedvig.android.design.system.hedvig.HedvigNotificationCard import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigTheme @@ -12,7 +12,6 @@ import com.hedvig.android.design.system.hedvig.NotificationDefaults import com.hedvig.android.design.system.hedvig.NotificationDefaults.InfoCardStyle.Default import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority.Info import com.hedvig.android.design.system.hedvig.ProvideTextStyle -import com.hedvig.android.design.system.hedvig.RichText import com.hedvig.android.design.system.hedvig.Surface import hedvig.resources.Res import hedvig.resources.general_close_button @@ -28,11 +27,7 @@ internal fun ChatBanner( ProvideTextStyle(HedvigTheme.typography.label.copy(color = HedvigTheme.colorScheme.signalBlueText)) { HedvigNotificationCard( content = { - RichText { - Markdown( - content = text, - ) - } + HedvigMarkdownText(content = text) }, priority = Info, modifier = modifier diff --git a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/ui/TextWithClickableUrls.kt b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/ui/TextWithClickableUrls.kt index a511d9b6ed..4edfc19aa3 100644 --- a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/ui/TextWithClickableUrls.kt +++ b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/ui/TextWithClickableUrls.kt @@ -4,9 +4,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.TextStyle -import com.halilibo.richtext.commonmark.Markdown +import com.hedvig.android.design.system.hedvig.HedvigMarkdownText import com.hedvig.android.design.system.hedvig.ProvideTextStyle -import com.hedvig.android.design.system.hedvig.RichText @OptIn(ExperimentalTextApi::class) @Composable @@ -14,12 +13,9 @@ internal fun TextWithClickableUrls(text: String, modifier: Modifier = Modifier, ProvideTextStyle( style, ) { - RichText( + HedvigMarkdownText( + content = text, modifier = modifier, - ) { - Markdown( - content = text, - ) - } + ) } } diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/data/PetContractForChipId.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/data/PetContractForChipId.kt index 06d83561a6..12e8dc4dd3 100644 --- a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/data/PetContractForChipId.kt +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/data/PetContractForChipId.kt @@ -6,5 +6,5 @@ data class PetContractForChipId( val id: String, val displayName: String, val contractExposure: String, - val contractGroup: ContractGroup + val contractGroup: ContractGroup, ) diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/navigation/ChipIdGraph.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/navigation/ChipIdGraph.kt index 385810a8cc..6f25add2a0 100644 --- a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/navigation/ChipIdGraph.kt +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/navigation/ChipIdGraph.kt @@ -23,7 +23,7 @@ fun NavGraphBuilder.chipIdGraph( navigateUp: () -> Unit, hedvigDeepLinkContainer: HedvigDeepLinkContainer, popBackStackOrFinish: () -> Unit, - goHome: () -> Unit + goHome: () -> Unit, ) { navdestination( deepLinks = navDeepLinks( @@ -88,10 +88,8 @@ fun NavGraphBuilder.chipIdGraph( if (!navController.popBackStack(ChipIdGraphDestination::class, inclusive = true)) { goHome() } - } + }, ) } } } - - diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/navigation/ChipIdNavDestination.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/navigation/ChipIdNavDestination.kt index 891de2fad8..f7f0fdd418 100644 --- a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/navigation/ChipIdNavDestination.kt +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/navigation/ChipIdNavDestination.kt @@ -18,18 +18,18 @@ internal sealed interface ChipIdDestination { @androidx.annotation.Keep @Serializable data class AddChipId( - val contractId: String + val contractId: String, ) : ChipIdDestination, Destination @androidx.annotation.Keep @Serializable data object SelectInsuranceForChipId : ChipIdDestination, Destination + @androidx.annotation.Keep @Serializable data class AddChipIdTriage( /** Must match the name of the param inside [com.hedvig.android.navigation.core.HedvigDeepLinkContainer.petIdWithContractId] */ @SerialName("contractId") - val contractId: String? = null + val contractId: String? = null, ) : ChipIdDestination, Destination } - diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/AddChipIdScreen.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/AddChipIdScreen.kt index a23ddeb4c5..81ea360003 100644 --- a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/AddChipIdScreen.kt +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/AddChipIdScreen.kt @@ -181,8 +181,7 @@ private fun ColumnScope.AddChipIdContent( text = uiState.chipIdText, labelText = stringResource(Res.string.CHIP_ID_LABEL), updateText = updateText, - - ) + ) AnimatedContent( targetState = uiState.errorType, @@ -222,11 +221,7 @@ private fun ColumnScope.AddChipIdContent( } @Composable -private fun ChipIdTextField( - text: String, - labelText: String, - updateText: (String) -> Unit, -) { +private fun ChipIdTextField(text: String, labelText: String, updateText: (String) -> Unit) { val interactionSource = remember { MutableInteractionSource() } var input by remember { mutableStateOf(text) } val mask = "000-000-000-000-000" @@ -300,12 +295,8 @@ private class ChipIdVisualTransformation( } } - @Composable -private fun InsuranceInfoCard( - insuranceInfo: PetContractForChipId, - modifier: Modifier = Modifier, -) { +private fun InsuranceInfoCard(insuranceInfo: PetContractForChipId, modifier: Modifier = Modifier) { HedvigCard( modifier .border( @@ -352,7 +343,6 @@ private fun PreviewTerminationConfirmationScreen( } } - private class AddChipIdScreenStateProvider : CollectionPreviewParameterProvider( listOf( AddChipIdUiState.Error, diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/AddChipIdViewModel.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/AddChipIdViewModel.kt index c1b7351d85..611ebbbcc2 100644 --- a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/AddChipIdViewModel.kt +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/AddChipIdViewModel.kt @@ -20,13 +20,13 @@ internal class AddChipIdViewModel( getContractsWithMissingChipIdUseCase: GetContractsWithMissingChipIdUseCase, contractId: String, ) : MoleculeViewModel( - initialState = AddChipIdUiState.Loading, - presenter = AddChipIdPresenter( - updateChipIdUseCase = updateChipIdUseCase, - contractId = contractId, - getContractsWithMissingChipIdUseCase = getContractsWithMissingChipIdUseCase, - ), -) + initialState = AddChipIdUiState.Loading, + presenter = AddChipIdPresenter( + updateChipIdUseCase = updateChipIdUseCase, + contractId = contractId, + getContractsWithMissingChipIdUseCase = getContractsWithMissingChipIdUseCase, + ), + ) internal class AddChipIdPresenter( private val updateChipIdUseCase: UpdateChipIdUseCase, @@ -81,8 +81,11 @@ internal class AddChipIdPresenter( Snapshot.withMutableSnapshot { val errorMessage = error.message submittingData = false - errorType = if (errorMessage==null) ChipIdErrorType.GeneralError - else ChipIdErrorType.ErrorWithMessage(errorMessage) + errorType = if (errorMessage == null) { + ChipIdErrorType.GeneralError + } else { + ChipIdErrorType.ErrorWithMessage(errorMessage) + } } }, ifRight = { @@ -130,6 +133,7 @@ internal class AddChipIdPresenter( submittingData = submittingData, errorType = errorType, ) + AddChipIdUiState.Error, AddChipIdUiState.Loading -> state } } @@ -167,5 +171,5 @@ internal sealed interface AddChipIdEvent { data object ShowedMessage : AddChipIdEvent - data class UpdateText(val newText: String): AddChipIdEvent + data class UpdateText(val newText: String) : AddChipIdEvent } diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/selectinsurance/SelectInsuranceForChipIdDestination.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/selectinsurance/SelectInsuranceForChipIdDestination.kt index 9dea55c037..e54dc16eb7 100644 --- a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/selectinsurance/SelectInsuranceForChipIdDestination.kt +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/selectinsurance/SelectInsuranceForChipIdDestination.kt @@ -219,6 +219,5 @@ private fun SelectInsuranceForChipIdContentScreen( ) Spacer(Modifier.height(16.dp)) } - } } diff --git a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/selectinsurance/SelectInsuranceForChipIdViewModel.kt b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/selectinsurance/SelectInsuranceForChipIdViewModel.kt index 0877a05ffc..21666491f3 100644 --- a/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/selectinsurance/SelectInsuranceForChipIdViewModel.kt +++ b/app/feature/feature-chip-id/src/main/kotlin/com/hedvig/android/feature/chip/id/ui/selectinsurance/SelectInsuranceForChipIdViewModel.kt @@ -32,9 +32,15 @@ internal class SelectInsuranceForChipIdPresenter( var currentState by remember { mutableStateOf(lastState) } var loadIteration by remember { mutableIntStateOf(0) } - var selectedContract: PetContractForChipId? by remember { mutableStateOf( - if (lastState is SelectInsuranceForChipIdState.Success) lastState.selectedContract else - null) } + var selectedContract: PetContractForChipId? by remember { + mutableStateOf( + if (lastState is SelectInsuranceForChipIdState.Success) { + lastState.selectedContract + } else { + null + }, + ) + } var contractIdToContinue: String? by remember { mutableStateOf(null) } LaunchedEffect(loadIteration) { diff --git a/app/feature/feature-choose-tier/build.gradle.kts b/app/feature/feature-choose-tier/build.gradle.kts index 2402676420..5cf5a14254 100644 --- a/app/feature/feature-choose-tier/build.gradle.kts +++ b/app/feature/feature-choose-tier/build.gradle.kts @@ -34,7 +34,7 @@ dependencies { implementation(projects.dataContract) implementation(projects.dataProductVariantPublic) implementation(projects.designSystemHedvig) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.languageCore) implementation(projects.moleculePublic) implementation(projects.navigationCommon) diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt index d4176b9424..b443d59be8 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt @@ -2,11 +2,11 @@ package com.hedvig.feature.claim.chat.data import com.hedvig.android.core.uidata.UiFile import com.hedvig.android.design.system.hedvig.DatePickerUiState +import com.hedvig.android.shared.partners.deflect.DeflectData import kotlin.jvm.JvmInline import kotlin.time.Instant import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable -import com.hedvig.android.shared.partners.deflect.DeflectData @JvmInline value class ClaimIntentId(val value: String) @@ -163,10 +163,9 @@ internal sealed interface StepContent { @Serializable data class Deflect( @Contextual - val deflectData: DeflectData + val deflectData: DeflectData, ) : StepContent { override val isSkippable: Boolean = false - } object Unknown : StepContent { diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt index aac1365519..81b697e6ae 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt @@ -178,7 +178,7 @@ private fun ClaimIntentStepContentFragment.toStepContent(locale: CommonLocale): faq = faq.map { it.toInfoBlock() }, buttonText = buttonTitle, ), - ) + ) } else -> { diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecordingStepSections.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecordingStepSections.kt index 062b4d1c25..4a063a4a89 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecordingStepSections.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecordingStepSections.kt @@ -393,7 +393,7 @@ private fun AudioRecordingSheetContent( audioRecordingState: AudioRecordingStepState.AudioRecording, stopRecording: () -> Unit, startRecording: () -> Unit, - recordAudioPermissionState: PermissionState + recordAudioPermissionState: PermissionState, ) { Column { HedvigText( @@ -442,7 +442,7 @@ private fun AudioRecordingSheetContent( text = stringResource(Res.string.something_went_wrong), modifier = Modifier, iconStyle = EmptyStateDefaults.EmptyStateIconStyle.ERROR, - description = null + description = null, ) } @@ -1179,6 +1179,7 @@ private class AudioRecordingSheetContentStateProvider : private class MockPermissionState(val granted: Boolean) : PermissionState { override val permission: String = "android.permission.RECORD_AUDIO" override val status: PermissionStatus = if (granted) PermissionStatus.Granted else PermissionStatus.Denied(false) + override fun launchPermissionRequest() {} } diff --git a/app/feature/feature-claim-chat/src/nativeMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewController.kt b/app/feature/feature-claim-chat/src/nativeMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewController.kt deleted file mode 100644 index a136d8db56..0000000000 --- a/app/feature/feature-claim-chat/src/nativeMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewController.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.hedvig.feature.claim.chat - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.ui.Modifier -import androidx.compose.ui.window.ComposeUIViewController -import platform.UIKit.UIViewController - -@Suppress("unused", "FunctionName") // Used from iOS -fun ClaimChatViewController(isDevelopmentFlow: Boolean, messageId: String?): UIViewController = - ComposeUIViewController { - Box(Modifier.fillMaxSize()) { - // Unused -// ClaimChatDestination() - } - } diff --git a/app/feature/feature-claim-details/build.gradle.kts b/app/feature/feature-claim-details/build.gradle.kts index ee7ec249c1..3c02bba014 100644 --- a/app/feature/feature-claim-details/build.gradle.kts +++ b/app/feature/feature-claim-details/build.gradle.kts @@ -45,7 +45,7 @@ dependencies { implementation(projects.dataCrossSellAfterClaimClosed) implementation(projects.dataDisplayItems) implementation(projects.designSystemHedvig) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.fileUploadUi) implementation(projects.moleculePublic) implementation(projects.navigationCommon) diff --git a/app/feature/feature-delete-account/build.gradle.kts b/app/feature/feature-delete-account/build.gradle.kts index f2fb37a565..d6bcfe5229 100644 --- a/app/feature/feature-delete-account/build.gradle.kts +++ b/app/feature/feature-delete-account/build.gradle.kts @@ -17,8 +17,6 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.apollo.normalizedCache) implementation(libs.arrow.core) - implementation(libs.compose.richtext) - implementation(libs.compose.richtextCommonmark) implementation(libs.coroutines.core) implementation(libs.jetbrains.lifecycle.runtime.compose) implementation(libs.koin.composeViewModel) @@ -30,7 +28,7 @@ dependencies { implementation(projects.coreCommonPublic) implementation(projects.coreResources) implementation(projects.designSystemHedvig) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.moleculePublic) implementation(projects.navigationCommon) implementation(projects.navigationCompose) diff --git a/app/feature/feature-delete-account/src/main/kotlin/com/hedvig/android/feature/deleteaccount/DeleteAccountDestination.kt b/app/feature/feature-delete-account/src/main/kotlin/com/hedvig/android/feature/deleteaccount/DeleteAccountDestination.kt index c425c35185..c7b8dad98e 100644 --- a/app/feature/feature-delete-account/src/main/kotlin/com/hedvig/android/feature/deleteaccount/DeleteAccountDestination.kt +++ b/app/feature/feature-delete-account/src/main/kotlin/com/hedvig/android/feature/deleteaccount/DeleteAccountDestination.kt @@ -12,16 +12,15 @@ 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.halilibo.richtext.commonmark.Markdown import com.hedvig.android.design.system.hedvig.ButtonDefaults import com.hedvig.android.design.system.hedvig.HedvigButton import com.hedvig.android.design.system.hedvig.HedvigErrorSection import com.hedvig.android.design.system.hedvig.HedvigFullScreenCenterAlignedProgress +import com.hedvig.android.design.system.hedvig.HedvigMarkdownText import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigScaffold import com.hedvig.android.design.system.hedvig.HedvigText import com.hedvig.android.design.system.hedvig.HedvigTheme -import com.hedvig.android.design.system.hedvig.RichText import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.feature.chat.DeleteAccountViewModel import hedvig.resources.DELETE_ACCOUNT_DELETE_ACCOUNT_DESCRIPTION @@ -122,15 +121,12 @@ private fun DeleteScreenContents( .padding(horizontal = 16.dp), ) Spacer(Modifier.height(32.dp)) - RichText( + HedvigMarkdownText( + content = description, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), - ) { - Markdown( - content = description, - ) - } + ) Spacer(Modifier.height(16.dp)) Spacer(Modifier.weight(1f)) Spacer(Modifier.height(8.dp)) diff --git a/app/feature/feature-help-center/build.gradle.kts b/app/feature/feature-help-center/build.gradle.kts index 2ff85470a2..a649f773eb 100644 --- a/app/feature/feature-help-center/build.gradle.kts +++ b/app/feature/feature-help-center/build.gradle.kts @@ -1,5 +1,8 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi + plugins { - id("hedvig.android.library") + id("hedvig.multiplatform.library") + id("hedvig.multiplatform.library.android") id("hedvig.gradle.plugin") } @@ -10,53 +13,71 @@ hedvig { serialization() } -dependencies { - api(libs.androidx.navigation.common) - - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.compose.foundation) - implementation(libs.androidx.navigation.compose) - implementation(libs.apollo.runtime) - implementation(libs.arrow.core) - implementation(libs.arrow.fx) - implementation(libs.compose.richtext) - implementation(libs.compose.richtextCommonmark) - implementation(libs.coroutines.core) - implementation(libs.jetbrains.lifecycle.runtime.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.coreResources) - implementation(projects.dataContract) - implementation(projects.dataConversations) - implementation(projects.dataTermination) - implementation(projects.designSystemHedvig) - implementation(projects.featureFlagsPublic) - implementation(projects.moleculePublic) - implementation(projects.navigationCommon) - implementation(projects.navigationCompose) - implementation(projects.navigationComposeTyped) - implementation(projects.navigationCore) - implementation(projects.uiEmergency) - implementation(projects.partnersDeflect) - - testImplementation(libs.apollo.testingSupport) - testImplementation(libs.assertK) - testImplementation(libs.coroutines.test) - testImplementation(libs.junit) - testImplementation(libs.turbine) - testImplementation(projects.apolloOctopusTest) - testImplementation(projects.apolloTest) - testImplementation(projects.coreCommonTest) - testImplementation(projects.featureFlagsTest) - testImplementation(projects.languageTest) - testImplementation(projects.loggingTest) - testImplementation(projects.memberRemindersTest) - testImplementation(projects.moleculeTest) - testImplementation(projects.testClock) +kotlin { + sourceSets { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + applyDefaultHierarchyTemplate { + common { + group("jvmAndAndroid") { + withAndroidLibraryTarget() + withJvm() + } + } + } + commonMain.dependencies { + implementation(libs.apollo.runtime) + implementation(libs.apollo.normalizedCache) + implementation(libs.arrow.core) + implementation(libs.arrow.fx) + implementation(libs.coroutines.core) + implementation(libs.jetbrains.compose.ui.tooling.preview) + implementation(libs.jetbrains.lifecycle.runtime.compose) + implementation(libs.jetbrains.navigation.common) + implementation(libs.jetbrains.navigation.compose) + implementation(libs.jetbrains.navigationevent.compose) + implementation(libs.koin.composeViewModel) + implementation(libs.koin.core) + implementation(libs.kotlinx.serialization.core) + implementation(libs.mikepenz.markdown) + implementation(projects.apolloCore) + implementation(projects.apolloOctopusPublic) + implementation(projects.composeUi) + implementation(projects.coreBuildConstants) + implementation(projects.coreCommonPublic) + implementation(projects.coreResources) + implementation(projects.dataContract) + implementation(projects.dataConversations) + implementation(projects.dataTermination) + implementation(projects.designSystemHedvig) + implementation(projects.featureFlags) + implementation(projects.moleculePublic) + implementation(projects.navigationCommon) + implementation(projects.navigationCompose) + implementation(projects.navigationComposeTyped) + implementation(projects.navigationCore) + implementation(projects.partnersDeflect) + implementation(projects.uiEmergency) + } + androidMain.dependencies { + implementation(libs.bundles.kmpPreviewBugWorkaround) + } + jvmMain.dependencies { + } + androidInstrumentedTest.dependencies { + implementation(libs.apollo.testingSupport) + implementation(libs.assertK) + implementation(libs.coroutines.test) + implementation(libs.junit) + implementation(libs.turbine) + implementation(projects.apolloOctopusTest) + implementation(projects.apolloTest) + implementation(projects.coreCommonTest) + implementation(projects.featureFlagsTest) + implementation(projects.languageTest) + implementation(projects.loggingTest) + implementation(projects.memberRemindersTest) + implementation(projects.moleculeTest) + implementation(projects.testClock) + } + } } diff --git a/app/feature/feature-help-center/src/commonMain/graphql/MutationPuppyGuideEngagement.graphql b/app/feature/feature-help-center/src/commonMain/graphql/MutationPuppyGuideEngagement.graphql new file mode 100644 index 0000000000..0964b67a59 --- /dev/null +++ b/app/feature/feature-help-center/src/commonMain/graphql/MutationPuppyGuideEngagement.graphql @@ -0,0 +1,5 @@ +mutation PuppyGuideEngagement($name: String!, $rating: Int, $read: Boolean) { + puppyGuideEngagement(engagement: {name: $name, rating: $rating, read: $read}) { + success + } +} diff --git a/app/feature/feature-help-center/src/main/graphql/QueryAvailableSelfServiceOnContracts.graphql b/app/feature/feature-help-center/src/commonMain/graphql/QueryAvailableSelfServiceOnContracts.graphql similarity index 100% rename from app/feature/feature-help-center/src/main/graphql/QueryAvailableSelfServiceOnContracts.graphql rename to app/feature/feature-help-center/src/commonMain/graphql/QueryAvailableSelfServiceOnContracts.graphql diff --git a/app/feature/feature-help-center/src/main/graphql/QueryHelpCenterFAQ.graphql b/app/feature/feature-help-center/src/commonMain/graphql/QueryHelpCenterFAQ.graphql similarity index 100% rename from app/feature/feature-help-center/src/main/graphql/QueryHelpCenterFAQ.graphql rename to app/feature/feature-help-center/src/commonMain/graphql/QueryHelpCenterFAQ.graphql diff --git a/app/feature/feature-help-center/src/main/graphql/QueryMemberActions.graphql b/app/feature/feature-help-center/src/commonMain/graphql/QueryMemberActions.graphql similarity index 100% rename from app/feature/feature-help-center/src/main/graphql/QueryMemberActions.graphql rename to app/feature/feature-help-center/src/commonMain/graphql/QueryMemberActions.graphql diff --git a/app/feature/feature-help-center/src/commonMain/graphql/QueryPuppyGuide.graphql b/app/feature/feature-help-center/src/commonMain/graphql/QueryPuppyGuide.graphql new file mode 100644 index 0000000000..420431124a --- /dev/null +++ b/app/feature/feature-help-center/src/commonMain/graphql/QueryPuppyGuide.graphql @@ -0,0 +1,17 @@ +query PuppyGuide { + currentMember { + puppyGuide { + forYoungDog + stories { + categories + content + image + name + rating + read + subtitle + title + } + } + } +} diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterGraph.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterGraph.kt similarity index 81% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterGraph.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterGraph.kt index cdb3f1a57c..33b0981745 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterGraph.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterGraph.kt @@ -15,6 +15,10 @@ import com.hedvig.android.feature.help.center.home.HelpCenterHomeDestination import com.hedvig.android.feature.help.center.navigation.HelpCenterDestination import com.hedvig.android.feature.help.center.navigation.HelpCenterDestinations import com.hedvig.android.feature.help.center.navigation.HelpCenterDestinations.Emergency +import com.hedvig.android.feature.help.center.puppyguide.PuppyArticleDestination +import com.hedvig.android.feature.help.center.puppyguide.PuppyArticleViewModel +import com.hedvig.android.feature.help.center.puppyguide.PuppyGuideDestination +import com.hedvig.android.feature.help.center.puppyguide.PuppyGuideViewModel import com.hedvig.android.feature.help.center.question.HelpCenterQuestionDestination import com.hedvig.android.feature.help.center.question.HelpCenterQuestionViewModel import com.hedvig.android.feature.help.center.topic.HelpCenterTopicDestination @@ -29,12 +33,13 @@ import org.koin.core.parameter.parametersOf fun NavGraphBuilder.helpCenterGraph( hedvigDeepLinkContainer: HedvigDeepLinkContainer, navController: NavController, + onNavigateUp: () -> Unit, onNavigateToQuickLink: (QuickLinkDestination.OuterDestination) -> Unit, onNavigateToInbox: () -> Unit, onNavigateToNewConversation: () -> Unit, openUrl: (String) -> Unit, tryToDialPhone: (String) -> Unit, - imageLoader: ImageLoader + imageLoader: ImageLoader, ) { navgraph( startDestination = HelpCenterDestinations.HelpCenter::class, @@ -80,7 +85,10 @@ fun NavGraphBuilder.helpCenterGraph( onNavigateToNewConversation = dropUnlessResumed { onNavigateToNewConversation() }, - onNavigateUp = navController::navigateUp, + onNavigateUp = onNavigateUp, + onNavigateToPuppyGuide = dropUnlessResumed { + navController.navigate(HelpCenterDestinations.PuppyGuide) + }, ) } @@ -126,6 +134,7 @@ fun NavGraphBuilder.helpCenterGraph( sections = sections, navigateUp = navController::navigateUp, navigateBack = navController::popBackStack, + openUrl = openUrl, ) } navdestination(HelpCenterDestinations.Emergency) { @@ -135,7 +144,38 @@ fun NavGraphBuilder.helpCenterGraph( openUrl = openUrl, tryToDialPhone = tryToDialPhone, onNavigateToNewConversation = dropUnlessResumed { onNavigateToNewConversation() }, - imageLoader = imageLoader + imageLoader = imageLoader, + ) + } + + navdestination( + deepLinks = navDeepLinks(hedvigDeepLinkContainer.puppyGuide), + ) { _ -> + val viewModel = koinViewModel() + PuppyGuideDestination( + viewModel, + onNavigateUp = navController::navigateUp, + onNavigateToArticle = { story -> + with(navController) { + navigate( + HelpCenterDestinations.PuppyGuideArticle( + story.name, + ), + ) + } + }, + imageLoader = imageLoader, + ) + } + + navdestination { + val viewModel = koinViewModel { + parametersOf(storyName) + } + PuppyArticleDestination( + viewModel = viewModel, + navigateUp = navController::navigateUp, + imageLoader = imageLoader, ) } } diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt similarity index 89% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt index 67b1c2d431..76d7ac8c5d 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt @@ -24,6 +24,7 @@ import com.hedvig.android.feature.help.center.HelpCenterUiState.Search import com.hedvig.android.feature.help.center.data.FAQItem import com.hedvig.android.feature.help.center.data.FAQTopic import com.hedvig.android.feature.help.center.data.GetHelpCenterFAQUseCase +import com.hedvig.android.feature.help.center.data.GetPuppyGuideUseCase import com.hedvig.android.feature.help.center.data.GetQuickLinksUseCase import com.hedvig.android.feature.help.center.data.QuickLinkDestination import com.hedvig.android.feature.help.center.model.QuickAction @@ -61,6 +62,7 @@ internal data class HelpCenterUiState( val search: Search?, val showNavigateToInboxButton: Boolean, val destinationToNavigate: QuickLinkDestination? = null, + val puppyGuide: PuppyGuidePresentation?, ) { data class QuickLink(val quickAction: QuickAction) @@ -72,6 +74,12 @@ internal data class HelpCenterUiState( data class QuickLinks(val quickLinks: NonEmptyList) : QuickLinkUiState } + sealed interface PuppyGuidePresentation { + data object FullCard : PuppyGuidePresentation + + data object QuickAction : PuppyGuidePresentation + } + data class Search( val searchQuery: String?, val activeSearchState: ActiveSearchState, @@ -93,6 +101,7 @@ internal class HelpCenterPresenter( private val getQuickLinksUseCase: GetQuickLinksUseCase, private val hasAnyActiveConversationUseCase: HasAnyActiveConversationUseCase, private val getHelpCenterFAQUseCase: GetHelpCenterFAQUseCase, + private val getPuppyGuideUseCase: GetPuppyGuideUseCase, ) : MoleculePresenter { @Composable override fun MoleculePresenterScope.present(lastState: HelpCenterUiState): HelpCenterUiState { @@ -161,7 +170,8 @@ internal class HelpCenterPresenter( combine( flow = flow { emit(getQuickLinksUseCase.invoke()) }, flow2 = flow { emit(getHelpCenterFAQUseCase.invoke()) }, - ) { quickLinks, faq -> + flow3 = getPuppyGuideUseCase.invoke(), + ) { quickLinks, faq, puppyGuideResult -> quickLinksUiState = quickLinks.fold( ifLeft = { HelpCenterUiState.QuickLinkUiState.NoQuickLinks @@ -179,12 +189,19 @@ internal class HelpCenterPresenter( ) val topics = faq.getOrNull()?.topics ?: listOf() val questions = faq.getOrNull()?.commonFAQ ?: listOf() + val puppyGuide = puppyGuideResult.getOrNull() + val puppyGuidePresentation = when { + puppyGuide == null || puppyGuide.stories.isEmpty() -> null + puppyGuide.isForYoungDog == true -> HelpCenterUiState.PuppyGuidePresentation.FullCard + else -> HelpCenterUiState.PuppyGuidePresentation.QuickAction + } currentState = currentState.copy( topics = topics, questions = questions, quickLinksUiState = quickLinksUiState, selectedQuickAction = selectedQuickAction, showNavigateToInboxButton = hasAnyActiveConversation, + puppyGuide = puppyGuidePresentation, ) }.collect() } diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt similarity index 84% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt index a7fb78c540..5c325fb3c3 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt @@ -2,6 +2,7 @@ package com.hedvig.android.feature.help.center import com.hedvig.android.data.conversations.HasAnyActiveConversationUseCase import com.hedvig.android.feature.help.center.data.GetHelpCenterFAQUseCase +import com.hedvig.android.feature.help.center.data.GetPuppyGuideUseCase import com.hedvig.android.feature.help.center.data.GetQuickLinksUseCase import com.hedvig.android.molecule.public.MoleculeViewModel @@ -9,6 +10,7 @@ internal class HelpCenterViewModel( getQuickLinksUseCase: GetQuickLinksUseCase, hasAnyActiveConversationUseCase: HasAnyActiveConversationUseCase, getHelpCenterFAQUseCase: GetHelpCenterFAQUseCase, + getPuppyGuideUseCase: GetPuppyGuideUseCase, ) : MoleculeViewModel( initialState = HelpCenterUiState( topics = listOf(), @@ -17,10 +19,12 @@ internal class HelpCenterViewModel( quickLinksUiState = HelpCenterUiState.QuickLinkUiState.Loading, search = null, showNavigateToInboxButton = false, + puppyGuide = null, ), presenter = HelpCenterPresenter( getQuickLinksUseCase = getQuickLinksUseCase, hasAnyActiveConversationUseCase = hasAnyActiveConversationUseCase, getHelpCenterFAQUseCase = getHelpCenterFAQUseCase, + getPuppyGuideUseCase = getPuppyGuideUseCase, ), ) diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/ShowNavigateToInboxViewModel.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/ShowNavigateToInboxViewModel.kt similarity index 100% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/ShowNavigateToInboxViewModel.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/ShowNavigateToInboxViewModel.kt diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/commonclaim/FirstVetDestination.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/commonclaim/FirstVetDestination.kt similarity index 81% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/commonclaim/FirstVetDestination.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/commonclaim/FirstVetDestination.kt index 9243d7033f..aed093100b 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/commonclaim/FirstVetDestination.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/commonclaim/FirstVetDestination.kt @@ -8,8 +8,13 @@ import com.hedvig.android.ui.emergency.FirstVetScreen import com.hedvig.android.ui.emergency.FirstVetSection @Composable -internal fun FirstVetDestination(sections: List, navigateUp: () -> Unit, navigateBack: () -> Unit) { - FirstVetScreen(sections, navigateUp, navigateBack) +internal fun FirstVetDestination( + sections: List, + navigateUp: () -> Unit, + navigateBack: () -> Unit, + openUrl: (String) -> Unit, +) { + FirstVetScreen(sections, navigateUp, navigateBack, openUrl) } @HedvigPreview @@ -34,6 +39,7 @@ private fun PreviewCommonClaimDestination() { ), {}, {}, + {}, ) } } diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/commonclaim/emergency/EmergencyDestination.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/commonclaim/emergency/EmergencyDestination.kt similarity index 100% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/commonclaim/emergency/EmergencyDestination.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/commonclaim/emergency/EmergencyDestination.kt diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetHelpCenterFAQUseCase.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetHelpCenterFAQUseCase.kt similarity index 100% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetHelpCenterFAQUseCase.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetHelpCenterFAQUseCase.kt diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetHelpCenterQuestionUseCase.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetHelpCenterQuestionUseCase.kt similarity index 100% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetHelpCenterQuestionUseCase.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetHelpCenterQuestionUseCase.kt diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetHelpCenterTopicUseCase.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetHelpCenterTopicUseCase.kt similarity index 100% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetHelpCenterTopicUseCase.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetHelpCenterTopicUseCase.kt diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetInsuranceForEditCoInsuredUseCase.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetInsuranceForEditCoInsuredUseCase.kt similarity index 100% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetInsuranceForEditCoInsuredUseCase.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetInsuranceForEditCoInsuredUseCase.kt diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt similarity index 71% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt index 7bc67ec078..a2a757b3f0 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt @@ -15,10 +15,6 @@ import com.hedvig.android.ui.emergency.FirstVetSection import kotlinx.coroutines.flow.first import octopus.MemberActionsQuery -internal interface GetMemberActionsUseCase { - suspend fun invoke(): Either -} - internal class GetMemberActionsUseCaseImpl( private val apolloClient: ApolloClient, private val featureManager: FeatureManager, @@ -59,36 +55,6 @@ internal class GetMemberActionsUseCaseImpl( } } -internal data class MemberAction( - val isCancelInsuranceEnabled: Boolean, - val isConnectPaymentEnabled: Boolean, - val isEditCoInsuredEnabled: Boolean, - val isEditCoOwnersEnabled: Boolean, - val isMovingEnabled: Boolean, - val isTravelCertificateEnabled: Boolean, - val isTierChangeEnabled: Boolean, - val sickAbroadAction: MemberActionWithDetails.SickAbroadAction?, - val firstVetAction: MemberActionWithDetails.FirstVetAction?, -) - -internal sealed interface MemberActionWithDetails { - data class SickAbroadAction( - val deflectData: DeflectData, - ) : MemberActionWithDetails - - data class FirstVetAction( - val sections: List, - ) : MemberActionWithDetails -} - -internal data class DeflectPartner( - val id: String, - val imageUrl: String?, - val phoneNumber: String?, - val url: String?, - val preferredImageHeight: Int?, -) - private fun MemberActionsQuery.Data.CurrentMember.MemberActions.FirstVetAction.toVetAction(): MemberActionWithDetails.FirstVetAction { val sections = this.sections.map { @@ -104,9 +70,9 @@ private fun MemberActionsQuery.Data.CurrentMember.MemberActions.FirstVetAction.t ) } -private fun MemberActionsQuery.Data.CurrentMember.MemberActions.SickAbroadDeflect?.toSickAbroadAction(): +private fun MemberActionsQuery.Data.CurrentMember.MemberActions.SickAbroadDeflect?.toSickAbroadAction(): MemberActionWithDetails.SickAbroadAction? { - if (this==null) return null + if (this == null) return null val partners = if (partners.isNotEmpty()) { DeflectData.DeflectPartnerContainer.ExtendedPartnerContainer( partners = partners.map { partner -> @@ -137,20 +103,20 @@ private fun MemberActionsQuery.Data.CurrentMember.MemberActions.SickAbroadDefle } val deflectData = DeflectData( - title = title, - infoText = infoText, - warningText = warningText, - partnersContainer = partners, - partnersInfo = partnersInfo?.let { - DeflectData.InfoBlock(it.title,it.description) - }, - content = content.let { - DeflectData.InfoBlock(it.title,it.description) - }, - faq = faq.map { faqItem -> - DeflectData.InfoBlock(faqItem.title,faqItem.description) - }, - buttonText = buttonTitle, - ) + title = title, + infoText = infoText, + warningText = warningText, + partnersContainer = partners, + partnersInfo = partnersInfo?.let { + DeflectData.InfoBlock(it.title, it.description) + }, + content = content.let { + DeflectData.InfoBlock(it.title, it.description) + }, + faq = faq.map { faqItem -> + DeflectData.InfoBlock(faqItem.title, faqItem.description) + }, + buttonText = buttonTitle, + ) return MemberActionWithDetails.SickAbroadAction(deflectData) } diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetPuppyGuideUseCase.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetPuppyGuideUseCase.kt new file mode 100644 index 0000000000..c93eac4f88 --- /dev/null +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetPuppyGuideUseCase.kt @@ -0,0 +1,69 @@ +package com.hedvig.android.feature.help.center.data + +import arrow.core.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.safeFlow +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.logger.logcat +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.serialization.Serializable +import octopus.PuppyGuideQuery + +interface GetPuppyGuideUseCase { + fun invoke(): Flow> +} + +internal class GetPuppyGuideUseCaseImpl( + private val apolloClient: ApolloClient, +) : GetPuppyGuideUseCase { + override fun invoke(): Flow> { + return apolloClient + .query(PuppyGuideQuery()) + .fetchPolicy(FetchPolicy.CacheAndNetwork) + .safeFlow(::ErrorMessage) + .map { either -> + either + .onLeft { logcat { "Cannot load PuppyGuide: $it" } } + .map { data -> + val puppyGuide = data.currentMember.puppyGuide + PuppyGuide( + stories = puppyGuide?.stories.orEmpty().map { story -> + PuppyGuideStory( + categories = story.categories, + content = story.content, + image = story.image, + name = story.name, + rating = story.rating, + isRead = story.read, + subtitle = story.subtitle, + title = story.title, + ) + }, + isForYoungDog = puppyGuide?.forYoungDog, + ) + } + } + } +} + +@Serializable +data class PuppyGuide( + val stories: List, + val isForYoungDog: Boolean?, +) + +@Serializable +data class PuppyGuideStory( + val categories: List, + val content: String, + val image: String, + val name: String, + val rating: Int?, + val isRead: Boolean, + val subtitle: String, + val title: String, +) diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetQuickLinksUseCase.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetQuickLinksUseCase.kt similarity index 96% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetQuickLinksUseCase.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetQuickLinksUseCase.kt index 88b57633ba..eacc044c8b 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetQuickLinksUseCase.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetQuickLinksUseCase.kt @@ -42,12 +42,16 @@ import hedvig.resources.Res import kotlinx.coroutines.flow.first import octopus.AvailableSelfServiceOnContractsQuery -internal class GetQuickLinksUseCase( +internal interface GetQuickLinksUseCase { + suspend fun invoke(): Either> +} + +internal class GetQuickLinksUseCaseImpl( private val apolloClient: ApolloClient, private val featureManager: FeatureManager, private val getMemberActionsUseCase: GetMemberActionsUseCase, -) { - suspend fun invoke(): Either> = either { +) : GetQuickLinksUseCase { + override suspend fun invoke(): Either> = either { val memberActionOptions = getMemberActionsUseCase.invoke().bind() buildList { @@ -142,12 +146,12 @@ internal class GetQuickLinksUseCase( ), ) } - if (memberActionOptions.sickAbroadAction!=null) { + if (memberActionOptions.sickAbroadAction != null) { val deflectData = memberActionOptions.sickAbroadAction.deflectData add( StandaloneQuickLink( quickLinkDestination = InnerHelpCenterDestination.QuickLinkSickAbroad( - deflectData + deflectData, ), titleRes = Res.string.HC_QUICK_ACTIONS_SICK_ABROAD_TITLE, hintTextRes = Res.string.HC_QUICK_ACTIONS_SICK_ABROAD_SUBTITLE, @@ -256,7 +260,7 @@ sealed interface QuickLinkDestination { internal sealed interface InnerHelpCenterDestination : QuickLinkDestination { data class QuickLinkSickAbroad( - val deflectData: DeflectData + val deflectData: DeflectData, ) : InnerHelpCenterDestination data class FirstVet( diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/MemberAction.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/MemberAction.kt new file mode 100644 index 0000000000..93bdf69269 --- /dev/null +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/MemberAction.kt @@ -0,0 +1,40 @@ +package com.hedvig.android.feature.help.center.data + +import arrow.core.Either +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.shared.partners.deflect.DeflectData +import com.hedvig.android.ui.emergency.FirstVetSection + +internal interface GetMemberActionsUseCase { + suspend fun invoke(): Either +} + +internal data class MemberAction( + val isCancelInsuranceEnabled: Boolean, + val isConnectPaymentEnabled: Boolean, + val isEditCoInsuredEnabled: Boolean, + val isEditCoOwnersEnabled: Boolean, + val isMovingEnabled: Boolean, + val isTravelCertificateEnabled: Boolean, + val isTierChangeEnabled: Boolean, + val sickAbroadAction: MemberActionWithDetails.SickAbroadAction?, + val firstVetAction: MemberActionWithDetails.FirstVetAction?, +) + +internal sealed interface MemberActionWithDetails { + data class SickAbroadAction( + val deflectData: DeflectData, + ) : MemberActionWithDetails + + data class FirstVetAction( + val sections: List, + ) : MemberActionWithDetails +} + +internal data class DeflectPartner( + val id: String, + val imageUrl: String?, + val phoneNumber: String?, + val url: String?, + val preferredImageHeight: Int?, +) diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/SetArticleRatingUseCase.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/SetArticleRatingUseCase.kt new file mode 100644 index 0000000000..82fcab84d1 --- /dev/null +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/SetArticleRatingUseCase.kt @@ -0,0 +1,34 @@ +package com.hedvig.android.feature.help.center.data + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.Optional +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import octopus.PuppyGuideEngagementMutation + +interface SetArticleRatingUseCase { + suspend fun invoke(articleName: String, rating: Int): Either +} + +internal class SetArticleRatingUseCaseImpl( + private val apolloClient: ApolloClient, +) : SetArticleRatingUseCase { + override suspend fun invoke(articleName: String, rating: Int): Either { + return either { + val data = apolloClient + .mutation( + PuppyGuideEngagementMutation( + name = articleName, + rating = Optional.present(rating), + ), + ) + .safeExecute() + .mapLeft { _ -> ErrorMessage() } + .bind() + ensure(data.puppyGuideEngagement.success) { ErrorMessage() } + } + } +} diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/SetArticleReadUseCase.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/SetArticleReadUseCase.kt new file mode 100644 index 0000000000..3aceb008fc --- /dev/null +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/SetArticleReadUseCase.kt @@ -0,0 +1,38 @@ +package com.hedvig.android.feature.help.center.data + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.Optional +import com.hedvig.android.apollo.ErrorMessage +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.logger.logcat +import octopus.PuppyGuideEngagementMutation + +interface SetArticleReadUseCase { + suspend fun invoke(articleName: String): Either +} + +internal class SetArticleReadUseCaseImpl( + private val apolloClient: ApolloClient, +) : SetArticleReadUseCase { + override suspend fun invoke(articleName: String): Either { + return either { + val data = apolloClient + .mutation( + PuppyGuideEngagementMutation( + name = articleName, + read = Optional.present(true), + ), + ) + .safeExecute() + .mapLeft(::ErrorMessage) + .bind() + ensure(data.puppyGuideEngagement.success) { ErrorMessage() } + } + .onLeft { logcat { "setArticleReadUseCase failed!" } } + .onRight { logcat { "setArticleReadUseCase set!" } } + } +} diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt similarity index 68% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt index 053df4eaa4..f458898500 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt @@ -14,7 +14,16 @@ import com.hedvig.android.feature.help.center.data.GetInsuranceForEditCoInsuredU import com.hedvig.android.feature.help.center.data.GetInsuranceForEditCoInsuredUseCaseImpl import com.hedvig.android.feature.help.center.data.GetMemberActionsUseCase import com.hedvig.android.feature.help.center.data.GetMemberActionsUseCaseImpl +import com.hedvig.android.feature.help.center.data.GetPuppyGuideUseCase +import com.hedvig.android.feature.help.center.data.GetPuppyGuideUseCaseImpl import com.hedvig.android.feature.help.center.data.GetQuickLinksUseCase +import com.hedvig.android.feature.help.center.data.GetQuickLinksUseCaseImpl +import com.hedvig.android.feature.help.center.data.SetArticleRatingUseCase +import com.hedvig.android.feature.help.center.data.SetArticleRatingUseCaseImpl +import com.hedvig.android.feature.help.center.data.SetArticleReadUseCase +import com.hedvig.android.feature.help.center.data.SetArticleReadUseCaseImpl +import com.hedvig.android.feature.help.center.puppyguide.PuppyArticleViewModel +import com.hedvig.android.feature.help.center.puppyguide.PuppyGuideViewModel import com.hedvig.android.feature.help.center.question.HelpCenterQuestionViewModel import com.hedvig.android.feature.help.center.topic.HelpCenterTopicViewModel import com.hedvig.android.featureflags.FeatureManager @@ -31,8 +40,12 @@ val helpCenterModule = module { GetHelpCenterTopicUseCaseImpl(get()) } + single { + GetPuppyGuideUseCaseImpl(get()) + } + single { - GetQuickLinksUseCase( + GetQuickLinksUseCaseImpl( apolloClient = get(), featureManager = get(), getMemberActionsUseCase = get(), @@ -54,6 +67,7 @@ val helpCenterModule = module { getQuickLinksUseCase = get(), hasAnyActiveConversationUseCase = get(), getHelpCenterFAQUseCase = get(), + getPuppyGuideUseCase = get(), ) } @@ -83,4 +97,25 @@ val helpCenterModule = module { hasAnyActiveConversationUseCase = get(), ) } + + viewModel { + PuppyGuideViewModel(getPuppyGuideUseCase = get()) + } + + viewModel { params -> + PuppyArticleViewModel( + getPuppyGuideUseCase = get(), + setArticleRatingUseCase = get(), + setArticleReadUseCase = get(), + storyName = params.get(), + ) + } + + single { + SetArticleRatingUseCaseImpl(apolloClient = get()) + } + + single { + SetArticleReadUseCaseImpl(apolloClient = get()) + } } diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt similarity index 74% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt index 5c76ddcce0..223654d697 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt @@ -1,6 +1,5 @@ package com.hedvig.android.feature.help.center.home -import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Spring @@ -13,28 +12,26 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -45,15 +42,14 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -62,13 +58,17 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.compose.dropUnlessResumed +import androidx.navigationevent.NavigationEventInfo +import androidx.navigationevent.compose.NavigationEventHandler +import androidx.navigationevent.compose.rememberNavigationEventState import arrow.core.toNonEmptyListOrNull import com.hedvig.android.compose.ui.plus import com.hedvig.android.compose.ui.preview.PreviewContentWithProvidedParametersAnimatedOnClick -import com.hedvig.android.compose.ui.withoutPlacement import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize.Large import com.hedvig.android.design.system.hedvig.DialogDefaults import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigButtonGhostWithBorder import com.hedvig.android.design.system.hedvig.HedvigCard import com.hedvig.android.design.system.hedvig.HedvigDialog import com.hedvig.android.design.system.hedvig.HedvigErrorSection @@ -76,11 +76,10 @@ import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigText import com.hedvig.android.design.system.hedvig.HedvigTextButton import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.HighlightLabel +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightColor import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightShade.LIGHT -import com.hedvig.android.design.system.hedvig.Icon -import com.hedvig.android.design.system.hedvig.IconButton -import com.hedvig.android.design.system.hedvig.LocalContentColor import com.hedvig.android.design.system.hedvig.RadioGroup import com.hedvig.android.design.system.hedvig.RadioOption import com.hedvig.android.design.system.hedvig.RadioOptionId @@ -88,13 +87,11 @@ import com.hedvig.android.design.system.hedvig.SearchField import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.design.system.hedvig.TopAppBarWithBack import com.hedvig.android.design.system.hedvig.clearFocusOnTap -import com.hedvig.android.design.system.hedvig.icon.Close -import com.hedvig.android.design.system.hedvig.icon.HedvigIcons -import com.hedvig.android.design.system.hedvig.icon.Search import com.hedvig.android.design.system.hedvig.placeholder.fade import com.hedvig.android.design.system.hedvig.placeholder.hedvigPlaceholder import com.hedvig.android.feature.help.center.HelpCenterEvent import com.hedvig.android.feature.help.center.HelpCenterUiState +import com.hedvig.android.feature.help.center.HelpCenterUiState.PuppyGuidePresentation import com.hedvig.android.feature.help.center.HelpCenterViewModel import com.hedvig.android.feature.help.center.data.FAQItem import com.hedvig.android.feature.help.center.data.FAQTopic @@ -106,7 +103,6 @@ import com.hedvig.android.feature.help.center.ui.HelpCenterSection import com.hedvig.android.feature.help.center.ui.HelpCenterSectionWithClickableRows import com.hedvig.android.feature.help.center.ui.StillNeedHelpSection import com.hedvig.android.placeholder.PlaceholderHighlight -import hedvig.resources.GENERAL_REMOVE import hedvig.resources.HC_CLAIMS_A_01 import hedvig.resources.HC_CLAIMS_Q_01 import hedvig.resources.HC_COMMON_QUESTIONS_TITLE @@ -118,13 +114,19 @@ import hedvig.resources.HC_QUICK_ACTIONS_CANCELLATION_TITLE import hedvig.resources.HC_QUICK_ACTIONS_EDIT_INSURANCE_TITLE import hedvig.resources.HC_QUICK_ACTIONS_TITLE import hedvig.resources.HC_TITLE +import hedvig.resources.PUPPY_GUIDE_GO_BUTTON +import hedvig.resources.PUPPY_GUIDE_LABEL +import hedvig.resources.PUPPY_GUIDE_SUBTITLE +import hedvig.resources.PUPPY_GUIDE_TITLE import hedvig.resources.Res import hedvig.resources.SEARCH_NOTHING_FOUND -import hedvig.resources.SEARCH_PLACEHOLDER import hedvig.resources.general_cancel_button import hedvig.resources.general_continue_button +import hedvig.resources.hundar_badar_pet +import hedvig.resources.pillow_hedvig import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString +import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @Composable @@ -136,6 +138,7 @@ internal fun HelpCenterHomeDestination( onNavigateUp: () -> Unit, onNavigateToInbox: () -> Unit, onNavigateToNewConversation: () -> Unit, + onNavigateToPuppyGuide: () -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(uiState.destinationToNavigate) { @@ -175,6 +178,8 @@ internal fun HelpCenterHomeDestination( reload = { viewModel.emit(HelpCenterEvent.ReloadFAQAndQuickLinks) }, + puppyGuide = uiState.puppyGuide, + onNavigateToPuppyGuide = onNavigateToPuppyGuide, ) } @@ -183,6 +188,7 @@ private fun HelpCenterHomeScreen( search: HelpCenterUiState.Search?, topics: List, questions: List, + puppyGuide: PuppyGuidePresentation?, quickLinksUiState: HelpCenterUiState.QuickLinkUiState, selectedQuickAction: QuickAction?, onNavigateToTopic: (topicId: String) -> Unit, @@ -197,6 +203,7 @@ private fun HelpCenterHomeScreen( onUpdateSearchResults: (String, HelpCenterUiState.HelpSearchResults?) -> Unit, onClearSearch: () -> Unit, reload: () -> Unit, + onNavigateToPuppyGuide: () -> Unit, ) { when (selectedQuickAction) { is StandaloneQuickLink -> { @@ -289,9 +296,9 @@ private fun HelpCenterHomeScreen( focusRequester = focusRequester, modifier = Modifier .padding(horizontal = 16.dp) - .windowInsetsPadding( - WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal), - ), + .windowInsetsPadding(WindowInsets.statusBars.only(WindowInsetsSides.Horizontal)) + .windowInsetsPadding(WindowInsets.captionBar.only(WindowInsetsSides.Horizontal)) + .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), onSearchChange = { if (it.isEmpty()) { searchQuery = null @@ -321,7 +328,7 @@ private fun HelpCenterHomeScreen( onClearSearch() }, ) - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(24.dp)) Column( modifier = Modifier .fillMaxSize() @@ -345,6 +352,8 @@ private fun HelpCenterHomeScreen( showNavigateToInboxButton = showNavigateToInboxButton, onNavigateToInbox = onNavigateToInbox, onNavigateToNewConversation = onNavigateToNewConversation, + puppyGuide = puppyGuide, + onNavigateToPuppyGuide = onNavigateToPuppyGuide, ) } else { SearchResults( @@ -371,31 +380,53 @@ private fun ContentWithoutSearch( topics: List, onNavigateToTopic: (topicId: String) -> Unit, questions: List, + puppyGuide: PuppyGuidePresentation?, onNavigateToQuestion: (questionId: String) -> Unit, showNavigateToInboxButton: Boolean, onNavigateToInbox: () -> Unit, onNavigateToNewConversation: () -> Unit, + onNavigateToPuppyGuide: () -> Unit, ) { + val showPuppyGuideQuickAction = puppyGuide is PuppyGuidePresentation.QuickAction Column { Column( modifier = Modifier.padding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal).asPaddingValues()), ) { - Spacer(Modifier.height(32.dp)) - Image( - painter = painterResource(hedvig.resources.R.drawable.pillow_hedvig), - contentDescription = null, - modifier = Modifier - .size(170.dp) - .align(Alignment.CenterHorizontally), - ) + AnimatedContent( + puppyGuide is PuppyGuidePresentation.FullCard, + contentAlignment = Alignment.Center, + ) { showFullCard -> + Column( + Modifier.fillMaxWidth(), + ) { + if (showFullCard) { + PuppyGuideCard( + onClick = dropUnlessResumed { onNavigateToPuppyGuide() }, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } else { + Spacer(Modifier.padding(16.dp)) + Image( + painter = painterResource(Res.drawable.pillow_hedvig), + contentDescription = null, + modifier = Modifier + .size(170.dp) + .align(Alignment.CenterHorizontally), + ) + } + } + } Spacer(Modifier.height(50.dp)) Column( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .padding(horizontal = 20.dp), ) { - HedvigText(stringResource(Res.string.HC_HOME_VIEW_QUESTION)) + HedvigText(stringResource(Res.string.HC_HOME_VIEW_QUESTION), + modifier = Modifier.semantics { + heading() + }) HedvigText( text = stringResource(Res.string.HC_HOME_VIEW_ANSWER), color = HedvigTheme.colorScheme.textSecondary, @@ -405,12 +436,18 @@ private fun ContentWithoutSearch( Column { AnimatedVisibility( - visible = quickLinksUiState !is HelpCenterUiState.QuickLinkUiState.NoQuickLinks, + visible = quickLinksUiState !is HelpCenterUiState.QuickLinkUiState.NoQuickLinks || + showPuppyGuideQuickAction, enter = QuickLinksSectionEnterTransition, exit = QuickLinksSectionExitTransition, ) { Column { - QuickLinksSection(quickLinksUiState, onQuickActionsSelected) + QuickLinksSection( + quickLinksUiState = quickLinksUiState, + onQuickActionsClick = onQuickActionsSelected, + showPuppyGuideRow = showPuppyGuideQuickAction, + onPuppyGuideClick = onNavigateToPuppyGuide, + ) Spacer(Modifier.height(32.dp)) } } @@ -437,7 +474,6 @@ private fun ContentWithoutSearch( Spacer(Modifier.height(32.dp)) } } - LocalConfiguration.current AnimatedVisibility( !questions.isEmpty(), ) { @@ -464,6 +500,54 @@ private fun ContentWithoutSearch( } } +@Composable +private fun PuppyGuideCard(onClick: () -> Unit, modifier: Modifier = Modifier) { + HedvigCard( + color = HedvigTheme.colorScheme.backgroundPrimary, + borderColor = HedvigTheme.colorScheme.borderSecondary, + modifier = modifier + .fillMaxWidth() + .shadow(1.dp, HedvigTheme.shapes.cornerXLarge), + ) { + Column{ + Box(Modifier.align(Alignment.CenterHorizontally)) { + Image( + painter = painterResource(Res.drawable.hundar_badar_pet), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .height(182.dp) + .clip(HedvigTheme.shapes.cornerXLargeTop), + ) + HighlightLabel( + stringResource(Res.string.PUPPY_GUIDE_LABEL), + size = HighlightLabelDefaults.HighLightSize.Small, + color = HighlightColor.Pink(LIGHT), + modifier = Modifier.padding(top = 16.dp, start = 16.dp), + ) + } + + Spacer(Modifier.height(16.dp)) + HedvigText( + stringResource(Res.string.PUPPY_GUIDE_TITLE), + modifier = Modifier.padding(horizontal = 16.dp), + ) + HedvigText( + stringResource(Res.string.PUPPY_GUIDE_SUBTITLE), + modifier = Modifier.padding(horizontal = 16.dp), + color = HedvigTheme.colorScheme.textSecondary, + ) + Spacer(Modifier.height(16.dp)) + HedvigButtonGhostWithBorder( + stringResource(Res.string.PUPPY_GUIDE_GO_BUTTON), + onClick = onClick, + modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(), + ) + Spacer(Modifier.height(16.dp)) + } + } +} + @Composable private fun SearchResults( activeSearchState: HelpCenterUiState.ActiveSearchState, @@ -471,7 +555,7 @@ private fun SearchResults( onNavigateToQuestion: (questionId: String) -> Unit, onQuickActionsSelected: (QuickAction) -> Unit, ) { - BackHandler(true) { + NavigationEventHandler(state = rememberNavigationEventState(NavigationEventInfo.None), isBackEnabled = true) { onBackPressed() } @@ -553,6 +637,8 @@ private val QuickLinksSectionExitTransition = fadeOut() + shrinkVertically( private fun QuickLinksSection( quickLinksUiState: HelpCenterUiState.QuickLinkUiState, onQuickActionsClick: (QuickAction) -> Unit, + showPuppyGuideRow: Boolean, + onPuppyGuideClick: () -> Unit, ) { HelpCenterSection( modifier = Modifier.padding(horizontal = 16.dp), @@ -563,42 +649,76 @@ private fun QuickLinksSection( targetState = quickLinksUiState, transitionSpec = { fadeIn() togetherWith fadeOut() }, ) { quickLinks: HelpCenterUiState.QuickLinkUiState -> - if (quickLinks is HelpCenterUiState.QuickLinkUiState.QuickLinks) { - Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - for (quickLink in quickLinks.quickLinks) { - QuickLinkCard( - topText = { - HedvigText( - text = stringResource( - quickLink.quickAction.titleRes, - ), - textAlign = TextAlign.Start, - ) - }, - bottomText = { - HedvigText( - text = stringResource( - quickLink.quickAction.hintTextRes, - ), - textAlign = TextAlign.Start, - color = HedvigTheme.colorScheme.textSecondary, - style = HedvigTheme.typography.finePrint, - ) - }, - onClick = { - onQuickActionsClick(quickLink.quickAction) - }, - ) + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + when (quickLinks) { + is HelpCenterUiState.QuickLinkUiState.QuickLinks -> { + for (quickLink in quickLinks.quickLinks) { + QuickLinkCard( + topText = { + HedvigText( + text = stringResource( + quickLink.quickAction.titleRes, + ), + textAlign = TextAlign.Start, + ) + }, + bottomText = { + HedvigText( + text = stringResource( + quickLink.quickAction.hintTextRes, + ), + textAlign = TextAlign.Start, + color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.finePrint, + ) + }, + onClick = { + onQuickActionsClick(quickLink.quickAction) + }, + ) + } + if (showPuppyGuideRow) { + PuppyGuideQuickLinkRow(onClick = onPuppyGuideClick) + } + } + + HelpCenterUiState.QuickLinkUiState.NoQuickLinks -> { + if (showPuppyGuideRow) { + PuppyGuideQuickLinkRow(onClick = onPuppyGuideClick) + } + } + + HelpCenterUiState.QuickLinkUiState.Loading -> { + PlaceholderQuickLinks() } } - } else { - PlaceholderQuickLinks() } } }, ) } +@Composable +private fun PuppyGuideQuickLinkRow(onClick: () -> Unit) { + QuickLinkCard( + topText = { + HedvigText( + text = stringResource(Res.string.PUPPY_GUIDE_TITLE), + textAlign = TextAlign.Start, + ) + }, + bottomText = { + HedvigText( + text = stringResource(Res.string.PUPPY_GUIDE_SUBTITLE), + textAlign = TextAlign.Start, + color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.finePrint, + ) + }, + onClick = onClick, + ) +} + @Composable private fun PlaceholderQuickLinks() { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { @@ -727,6 +847,8 @@ private fun PreviewHelpCenterHomeScreen( onUpdateSearchResults = { _, _ -> }, search = null, reload = {}, + puppyGuide = PuppyGuidePresentation.FullCard, + onNavigateToPuppyGuide = {}, ) } } @@ -772,6 +894,8 @@ private fun PreviewQuickLinkAnimations() { onUpdateSearchResults = { _, _ -> }, search = null, reload = {}, + puppyGuide = null, + onNavigateToPuppyGuide = {}, ) } } @@ -801,11 +925,77 @@ private fun PreviewQuickLinkEmptyState() { onUpdateSearchResults = { _, _ -> }, search = null, reload = {}, + puppyGuide = null, + onNavigateToPuppyGuide = {}, + ) + } + } +} + +@HedvigPreview +@Composable +private fun PreviewHelpCenterPuppyGuideVariants( + @PreviewParameter(PuppyGuidePresentationPreviewProvider::class) puppyGuide: PuppyGuidePresentation?, +) { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + HelpCenterHomeScreen( + topics = listOf( + FAQTopic( + title = "Payments", + commonFAQ = listOf(), + otherFAQ = listOf(), + id = "topicId", + ), + ), + questions = listOf( + FAQItem( + "01", + stringResource(Res.string.HC_CLAIMS_Q_01), + stringResource(Res.string.HC_CLAIMS_A_01), + ), + ), + selectedQuickAction = null, + onNavigateToTopic = {}, + onNavigateToQuestion = {}, + onNavigateToQuickLink = {}, + onQuickActionsSelected = {}, + onDismissQuickActionDialog = {}, + showNavigateToInboxButton = true, + onNavigateToInbox = {}, + onNavigateToNewConversation = {}, + onNavigateUp = {}, + quickLinksUiState = HelpCenterUiState.QuickLinkUiState.QuickLinks( + List(2) { + HelpCenterUiState.QuickLink( + StandaloneQuickLink( + Res.string.HC_QUICK_ACTIONS_CANCELLATION_TITLE, + Res.string.HC_QUICK_ACTIONS_CANCELLATION_SUBTITLE, + QuickLinkDestination.OuterDestination.QuickLinkTermination, + ), + ) + }.toNonEmptyListOrNull()!!, + ), + onClearSearch = {}, + onUpdateSearchResults = { _, _ -> }, + search = null, + reload = {}, + puppyGuide = puppyGuide, + onNavigateToPuppyGuide = {}, ) } } } +private class PuppyGuidePresentationPreviewProvider : + CollectionPreviewParameterProvider( + listOf( + null, + PuppyGuidePresentation.FullCard, + PuppyGuidePresentation.QuickAction, + ), + ) + private class QuickLinkUiStatePreviewProvider : CollectionPreviewParameterProvider( listOf( diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/model/QuickLink.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/model/QuickAction.kt similarity index 100% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/model/QuickLink.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/model/QuickAction.kt diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/navigation/HelpCenterDestination.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/navigation/HelpCenterDestination.kt similarity index 89% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/navigation/HelpCenterDestination.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/navigation/HelpCenterDestination.kt index 6845fc2eea..2866a17c96 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/navigation/HelpCenterDestination.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/navigation/HelpCenterDestination.kt @@ -33,7 +33,7 @@ internal sealed interface HelpCenterDestinations { @Serializable data class Emergency( - val deflectData: DeflectData + val deflectData: DeflectData, ) : HelpCenterDestinations, Destination { companion object : DestinationNavTypeAware { override val typeList: List = listOf(typeOf()) @@ -46,6 +46,12 @@ internal sealed interface HelpCenterDestinations { override val typeList: List = listOf(typeOf>()) } } + + @Serializable + data object PuppyGuide : HelpCenterDestinations, Destination + + @Serializable + data class PuppyGuideArticle(val storyName: String) : HelpCenterDestinations, Destination } val helpCenterCrossSellBottomSheetPermittingDestinations: List> = listOf( diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleDestination.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleDestination.kt new file mode 100644 index 0000000000..44f7c7f4a1 --- /dev/null +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleDestination.kt @@ -0,0 +1,333 @@ +package com.hedvig.android.feature.help.center.puppyguide + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.MutableWindowInsets +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.exclude +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.layout.windowInsetsTopHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.text.style.TextAlign +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 coil3.ImageLoader +import coil3.compose.AsyncImage +import com.hedvig.android.compose.ui.EmptyContentDescription +import com.hedvig.android.design.system.hedvig.HedvigCard +import com.hedvig.android.design.system.hedvig.HedvigErrorSection +import com.hedvig.android.design.system.hedvig.HedvigFullScreenCenterAlignedProgress +import com.hedvig.android.design.system.hedvig.HedvigShortMultiScreenPreview +import com.hedvig.android.design.system.hedvig.HedvigText +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.HorizontalItemsWithMaximumSpaceTaken +import com.hedvig.android.design.system.hedvig.ProvideTextStyle +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.rememberPreviewImageLoader +import com.hedvig.android.feature.help.center.data.PuppyGuideStory +import com.hedvig.android.feature.help.center.ui.MarkdownText +import com.hedvig.android.feature.help.center.ui.rememberPerformRatingHaptic +import hedvig.resources.PUPPY_GUIDE_RATING_NOT_HELPFUL +import hedvig.resources.PUPPY_GUIDE_RATING_QUESTION +import hedvig.resources.PUPPY_GUIDE_RATING_VERY_HELPFUL +import hedvig.resources.Res +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun PuppyArticleDestination( + viewModel: PuppyArticleViewModel, + navigateUp: () -> Unit, + imageLoader: ImageLoader, + onScrollOffsetChanged: (Float) -> Unit = {}, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + PuppyArticleScreen( + uiState, + navigateUp = navigateUp, + onReload = { + viewModel.emit(PuppyArticleEvent.Reload) + }, + imageLoader = imageLoader, + onRatingClick = { + viewModel.emit(PuppyArticleEvent.RatingClick(it)) + }, + onScrollOffsetChanged = onScrollOffsetChanged, + onReachedBottom = { + viewModel.emit(PuppyArticleEvent.ReachedBottom) + }, + ) +} + +@Composable +private fun PuppyArticleScreen( + uiState: PuppyArticleUiState, + navigateUp: () -> Unit, + onReload: () -> Unit, + onRatingClick: (Int) -> Unit, + imageLoader: ImageLoader, + onScrollOffsetChanged: (Float) -> Unit, + onReachedBottom: () -> Unit, +) { + when (uiState) { + PuppyArticleUiState.Failure -> PuppyScaffold(navigateUp = navigateUp) { + HedvigErrorSection( + onButtonClick = onReload, + modifier = Modifier.weight(1f), + ) + } + + PuppyArticleUiState.Loading -> HedvigFullScreenCenterAlignedProgress() + + is PuppyArticleUiState.Success -> PuppyArticleSuccessScreen( + uiState, + navigateUp = navigateUp, + imageLoader = imageLoader, + onRatingClick = onRatingClick, + onScrollOffsetChanged = onScrollOffsetChanged, + onReachedBottom = onReachedBottom, + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun PuppyArticleSuccessScreen( + uiState: PuppyArticleUiState.Success, + navigateUp: () -> Unit, + onRatingClick: (Int) -> Unit, + imageLoader: ImageLoader, + onScrollOffsetChanged: (Float) -> Unit, + onReachedBottom: () -> Unit, +) { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + Column(Modifier.fillMaxSize()) { + val consumedWindowInsets = remember { MutableWindowInsets() } + PuppyTopAppBar( + title = "", + onBack = navigateUp, + modifier = Modifier.onSizeChanged { + consumedWindowInsets.insets = WindowInsets(top = it.height) + }, + ) + val horizontalInsetsPadding = WindowInsets.safeDrawing + .only(WindowInsetsSides.Horizontal) + .asPaddingValues() + val scrollState = rememberScrollState() + val density = LocalDensity.current + LaunchedEffect(scrollState, density, onScrollOffsetChanged) { + snapshotFlow { with(density) { scrollState.value.toDp().value } }.collect(onScrollOffsetChanged) + } + val currentOnReachedBottom by rememberUpdatedState(onReachedBottom) + LaunchedEffect(scrollState) { + snapshotFlow { scrollState.maxValue } + .filter { it != Int.MAX_VALUE } + .debounce(150.milliseconds) + .first() + snapshotFlow { scrollState.value >= scrollState.maxValue }.first { it } + currentOnReachedBottom() + } + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(scrollState) + .padding(horizontalInsetsPadding) + .padding(horizontal = 16.dp), + ) { + Spacer( + modifier = Modifier.windowInsetsTopHeight( + WindowInsets.safeDrawing.exclude(consumedWindowInsets).only(WindowInsetsSides.Top), + ), + ) + Spacer(modifier = Modifier.height(8.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth(), + ) { + val fallbackPainter: Painter = ColorPainter(Color.Black.copy(alpha = 0.7f)) + AsyncImage( + model = uiState.story.image, + contentDescription = EmptyContentDescription, + placeholder = fallbackPainter, + error = fallbackPainter, + fallback = fallbackPainter, + imageLoader = imageLoader, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 200.dp) + .clip(HedvigTheme.shapes.cornerMedium), + ) + } + Spacer(Modifier.height(16.dp)) + HedvigText( + uiState.story.title, + style = HedvigTheme.typography.headlineMedium, + ) + Spacer(Modifier.height(4.dp)) + HedvigText( + uiState.story.subtitle, + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + ) + Spacer(Modifier.height(24.dp)) + ProvideTextStyle( + HedvigTheme.typography.bodySmall + .copy(color = HedvigTheme.colorScheme.textSecondaryTranslucent), + ) { + MarkdownText(uiState.story.content.replace(Regex("\n\\s*\n"), "\n\n\u200b\n\n"), withArticleStyle = true) + } + Spacer(Modifier.height(48.dp)) + HedvigText(stringResource(Res.string.PUPPY_GUIDE_RATING_QUESTION)) + Spacer(Modifier.height(16.dp)) + RatingSection( + onRatingClick = onRatingClick, + selectedRating = uiState.story.rating, + ) + Spacer(Modifier.height(16.dp)) + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) + } + } + } +} + +@Composable +private fun RatingSection(selectedRating: Int?, onRatingClick: (Int) -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val ratings = listOf(1, 2, 3, 4, 5) + val performRatingHaptic = rememberPerformRatingHaptic(LocalHapticFeedback.current) + Row( + horizontalArrangement = Arrangement.SpaceAround, + modifier = Modifier, + ) { + ratings.forEach { rating -> + val isSelectedRating = selectedRating == rating + HedvigCard( + modifier = Modifier.weight(1f), + onClick = { + performRatingHaptic(rating) + onRatingClick(rating) + }, + color = if (isSelectedRating) { + HedvigTheme.colorScheme.signalGreenFill + } else { + HedvigTheme.colorScheme.surfacePrimary + }, + ) { + HedvigText( + text = rating.toString(), + style = HedvigTheme.typography.bodyLarge, + color = if (isSelectedRating) { + HedvigTheme.colorScheme.textBlack + } else { + HedvigTheme.colorScheme.textSecondaryTranslucent + }, + textAlign = TextAlign.Center, + modifier = Modifier.padding(vertical = 16.dp), + ) + } + Spacer(Modifier.width(6.dp)) + } + } + Spacer(Modifier.height(8.dp)) + HorizontalItemsWithMaximumSpaceTaken( + startSlot = { + HedvigText( + stringResource(Res.string.PUPPY_GUIDE_RATING_NOT_HELPFUL), + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + ) + }, + endSlot = { + Row(horizontalArrangement = Arrangement.End) { + HedvigText( + stringResource(Res.string.PUPPY_GUIDE_RATING_VERY_HELPFUL), + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + ) + } + }, + spaceBetween = 8.dp, + ) + } +} + +@HedvigShortMultiScreenPreview +@Composable +private fun PuppyArticleScreenPreview( + @PreviewParameter(PuppyArticleUiStatePreviewProvider::class) uiState: PuppyArticleUiState, +) { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + PuppyArticleScreen( + uiState, + navigateUp = {}, + onReload = {}, + onRatingClick = {}, + imageLoader = rememberPreviewImageLoader(), + onScrollOffsetChanged = {}, + onReachedBottom = {}, + ) + } + } +} + +private class PuppyArticleUiStatePreviewProvider : + CollectionPreviewParameterProvider( + listOf( + PuppyArticleUiState.Success( + story = PuppyGuideStory( + categories = listOf("Food"), + content = "some long long long long long long long long long long long long" + + " long long long long long long long long long long long long content", + image = "", + name = "", + rating = 5, + isRead = false, + subtitle = "5 min read", + title = "Puppy food", + ), + ), + PuppyArticleUiState.Loading, + PuppyArticleUiState.Failure, + ), + ) diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleViewModel.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleViewModel.kt new file mode 100644 index 0000000000..e860c756f1 --- /dev/null +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleViewModel.kt @@ -0,0 +1,134 @@ +package com.hedvig.android.feature.help.center.puppyguide + +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.help.center.data.GetPuppyGuideUseCase +import com.hedvig.android.feature.help.center.data.PuppyGuideStory +import com.hedvig.android.feature.help.center.data.SetArticleRatingUseCase +import com.hedvig.android.feature.help.center.data.SetArticleReadUseCase +import com.hedvig.android.logger.logcat +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope +import com.hedvig.android.molecule.public.MoleculeViewModel + +internal class PuppyArticleViewModel( + getPuppyGuideUseCase: GetPuppyGuideUseCase, + setArticleRatingUseCase: SetArticleRatingUseCase, + setArticleReadUseCase: SetArticleReadUseCase, + storyName: String, +) : MoleculeViewModel( + presenter = PuppyArticlePresenter( + getPuppyGuideUseCase, + storyName, + setArticleRatingUseCase, + setArticleReadUseCase, + ), + initialState = PuppyArticleUiState.Loading, + ) + +private class PuppyArticlePresenter( + private val getPuppyGuideUseCase: GetPuppyGuideUseCase, + private val storyName: String, + private val setArticleRatingUseCase: SetArticleRatingUseCase, + private val setArticleReadUseCase: SetArticleReadUseCase, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present(lastState: PuppyArticleUiState): PuppyArticleUiState { + var currentState by remember { mutableStateOf(lastState) } + var loadIteration by remember { mutableIntStateOf(0) } + var rating by remember { mutableStateOf(null) } + var reachedBottom by remember { mutableStateOf(false) } + + CollectEvents { event -> + when (event) { + PuppyArticleEvent.Reload -> { + loadIteration++ + } + + is PuppyArticleEvent.RatingClick -> { + rating = event.rating + } + + PuppyArticleEvent.ReachedBottom -> { + reachedBottom = true + } + } + } + + LaunchedEffect(loadIteration) { + getPuppyGuideUseCase.invoke().collect { response -> + response.fold( + ifLeft = { + currentState = PuppyArticleUiState.Failure + }, + ifRight = { puppyGuide -> + val matchingStory = puppyGuide.stories.firstOrNull { it.name == storyName } + currentState = if (matchingStory == null) { + PuppyArticleUiState.Failure + } else { + rating = matchingStory.rating + PuppyArticleUiState.Success(matchingStory) + } + }, + ) + } + } + + LaunchedEffect(rating) { + val state = currentState as? PuppyArticleUiState.Success ?: return@LaunchedEffect + val currentRating = rating ?: return@LaunchedEffect + val articleName = state.story.name + setArticleRatingUseCase.invoke( + articleName = articleName, + rating = currentRating, + ).fold( + ifLeft = { logcat { "setArticleRatingUseCase rating failed!" } }, + ifRight = { logcat { "setArticleRatingUseCase rating set!" } }, + ) + } + + LaunchedEffect(reachedBottom) { + if (!reachedBottom) return@LaunchedEffect + val state = currentState as? PuppyArticleUiState.Success ?: return@LaunchedEffect + if (state.story.isRead) return@LaunchedEffect + setArticleReadUseCase.invoke(state.story.name) + } + + return when (val state = currentState) { + PuppyArticleUiState.Failure -> { + state + } + + PuppyArticleUiState.Loading -> { + state + } + + is PuppyArticleUiState.Success -> { + state.copy( + story = state.story.copy(rating = rating), + ) + } + } + } +} + +internal sealed interface PuppyArticleEvent { + data object Reload : PuppyArticleEvent + + data class RatingClick(val rating: Int) : PuppyArticleEvent + + data object ReachedBottom : PuppyArticleEvent +} + +internal sealed interface PuppyArticleUiState { + data class Success(val story: PuppyGuideStory) : PuppyArticleUiState + + data object Loading : PuppyArticleUiState + + data object Failure : PuppyArticleUiState +} diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideDestination.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideDestination.kt new file mode 100644 index 0000000000..077c2385b5 --- /dev/null +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideDestination.kt @@ -0,0 +1,513 @@ +package com.hedvig.android.feature.help.center.puppyguide + +import androidx.compose.foundation.Image +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.indication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.MutableWindowInsets +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.exclude +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil3.ImageLoader +import coil3.compose.AsyncImage +import com.hedvig.android.compose.ui.EmptyContentDescription +import com.hedvig.android.compose.ui.plus +import com.hedvig.android.compose.ui.rememberStickyHeaderTopInset +import com.hedvig.android.design.system.hedvig.ButtonDefaults +import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigErrorSection +import com.hedvig.android.design.system.hedvig.HedvigFullScreenCenterAlignedProgress +import com.hedvig.android.design.system.hedvig.HedvigShortMultiScreenPreview +import com.hedvig.android.design.system.hedvig.HedvigText +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.Icon +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.blockSwipeBackOnIos +import com.hedvig.android.design.system.hedvig.icon.Checkmark +import com.hedvig.android.design.system.hedvig.icon.HedvigIcons +import com.hedvig.android.design.system.hedvig.rememberPreviewImageLoader +import com.hedvig.android.feature.help.center.data.PuppyGuideStory +import hedvig.resources.A11Y_READ_LABEL_HACK +import hedvig.resources.PUPPY_GUIDE_INFO +import hedvig.resources.PUPPY_GUIDE_LABEL_READ +import hedvig.resources.PUPPY_GUIDE_TITLE +import hedvig.resources.Res +import hedvig.resources.hundar_badar_pet +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource + +@Composable +internal fun PuppyGuideDestination( + viewModel: PuppyGuideViewModel, + onNavigateUp: () -> Unit, + imageLoader: ImageLoader, + onNavigateToArticle: (PuppyGuideStory) -> Unit, + onScrollOffsetChanged: (Float) -> Unit = {}, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + PuppyGuideScreen( + uiState, + onNavigateToArticle = onNavigateToArticle, + onNavigateUp = onNavigateUp, + reload = { + viewModel.emit(PuppyGuideEvent.Reload) + }, + imageLoader = imageLoader, + onScrollOffsetChanged = onScrollOffsetChanged, + ) +} + +@Composable +private fun PuppyGuideScreen( + uiState: PuppyGuideUiState, + onNavigateToArticle: (PuppyGuideStory) -> Unit, + onNavigateUp: () -> Unit, + reload: () -> Unit, + imageLoader: ImageLoader, + onScrollOffsetChanged: (Float) -> Unit, +) { + when (uiState) { + PuppyGuideUiState.Failure -> PuppyScaffold(navigateUp = onNavigateUp) { + HedvigErrorSection( + onButtonClick = reload, + modifier = Modifier.weight(1f), + ) + } + + PuppyGuideUiState.Loading -> HedvigFullScreenCenterAlignedProgress() + + is PuppyGuideUiState.Success -> PuppyGuideSuccessScreen( + uiState, + onNavigateUp = onNavigateUp, + onNavigateToArticle = onNavigateToArticle, + imageLoader = imageLoader, + onScrollOffsetChanged = onScrollOffsetChanged, + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun PuppyGuideSuccessScreen( + uiState: PuppyGuideUiState.Success, + onNavigateToArticle: (PuppyGuideStory) -> Unit, + onNavigateUp: () -> Unit, + imageLoader: ImageLoader, + onScrollOffsetChanged: (Float) -> Unit, +) { + val categories = remember(uiState.stories) { uiState.stories.flatMap { it.categories }.toSet().toList() } + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + val density = LocalDensity.current + LaunchedEffect(listState, density, onScrollOffsetChanged) { + snapshotFlow { + if (listState.firstVisibleItemIndex > 0) { + 200f + } else { + with(density) { listState.firstVisibleItemScrollOffset.toDp().value } + } + }.collect(onScrollOffsetChanged) + } + + Surface( + color = HedvigTheme.colorScheme.backgroundPrimary, + ) { + Column( + Modifier.fillMaxSize(), + ) { + val consumedWindowInsets = remember { MutableWindowInsets() } + PuppyTopAppBar( + title = "", + onBack = onNavigateUp, + Modifier.onSizeChanged { + consumedWindowInsets.insets = WindowInsets(top = it.height) + }, + ) + val horizontalInsetsPadding = WindowInsets.safeDrawing + .only(WindowInsetsSides.Horizontal) + .asPaddingValues() + val verticalInsetsPadding = WindowInsets + .safeDrawing + .exclude(consumedWindowInsets) + .only(WindowInsetsSides.Vertical) + .asPaddingValues() + val sectionContentPadding = PaddingValues(horizontal = 16.dp) + horizontalInsetsPadding + + LazyColumn( + state = listState, + modifier = Modifier.fillMaxWidth(), + contentPadding = verticalInsetsPadding, + ) { + item { + Column( + modifier = Modifier.padding(sectionContentPadding), + ) { + Spacer(modifier = Modifier.height(8.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth(), + ) { + Image( + painter = painterResource(Res.drawable.hundar_badar_pet), + contentDescription = EmptyContentDescription, + contentScale = ContentScale.Crop, + alignment = Alignment.Center, + modifier = Modifier + .height(300.dp) + .clip(HedvigTheme.shapes.cornerXLarge), + ) + } + Spacer(modifier = Modifier.height(16.dp)) + HedvigText(stringResource(Res.string.PUPPY_GUIDE_TITLE)) + Spacer(modifier = Modifier.height(8.dp)) + HedvigText( + stringResource(Res.string.PUPPY_GUIDE_INFO), + color = HedvigTheme.colorScheme.textSecondary, + ) + Spacer(modifier = Modifier.height(48.dp)) + } + } + + stickyHeader(key = CategoriesStickyHeaderKey) { + val stickyTopPadding = rememberStickyHeaderTopInset( + listState = listState, + stickyHeaderKey = CategoriesStickyHeaderKey, + topContentPadding = verticalInsetsPadding.calculateTopPadding(), + ) + Surface( + color = HedvigTheme.colorScheme.backgroundPrimary, + modifier = Modifier.fillMaxWidth(), + ) { + Column(modifier = Modifier.padding(top = stickyTopPadding)) { + val hapticFeedback = LocalHapticFeedback.current + GuideCategoriesRow( + categories = categories, + contentPadding = sectionContentPadding, + onCategoryClick = onClick@{ category -> + hapticFeedback.performHapticFeedback(HapticFeedbackType.ContextClick) + val index = categories.indexOf(category) + if (index == -1) return@onClick + val stickyInfo = listState.layoutInfo.visibleItemsInfo + .firstOrNull { it.key == CategoriesStickyHeaderKey } + val stickyContentHeightPx = stickyInfo?.let { + val topInsetPx = with(density) { verticalInsetsPadding.calculateTopPadding().roundToPx() } + val currentTopPadding = (-it.offset).coerceIn(0, topInsetPx) + it.size - currentTopPadding + } ?: 0 + scope.launch { + listState.animateScrollToItem(index + 3, scrollOffset = -stickyContentHeightPx) + } + }, + ) + Spacer(modifier = Modifier.height(8.dp)) + } + } + } + + item { + Spacer(modifier = Modifier.height(16.dp)) + } + + items(categories) { cat -> + CategoryWithArticlesSection( + category = cat, + stories = uiState.stories.filter { it.categories.contains(cat) }, + onNavigateToArticle = onNavigateToArticle, + imageLoader = imageLoader, + contentPadding = sectionContentPadding, + ) + } + } + } + } +} + +@Composable +private fun GuideCategoriesRow( + categories: List, + contentPadding: PaddingValues, + onCategoryClick: (String) -> Unit, +) { + LazyRow( + modifier = Modifier.blockSwipeBackOnIos(), + contentPadding = contentPadding, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(categories) { category -> + HedvigButton( + text = category, + enabled = true, + buttonSize = ButtonDefaults.ButtonSize.Medium, + buttonStyle = ButtonDefaults.ButtonStyle.Secondary, + onClick = { + onCategoryClick(category) + }, + ) + } + } +} + +@Composable +private fun CategoryWithArticlesSection( + category: String, + stories: List, + onNavigateToArticle: (PuppyGuideStory) -> Unit, + imageLoader: ImageLoader, + contentPadding: PaddingValues, + modifier: Modifier = Modifier, +) { + Column(modifier) { + HedvigText( + category, + fontStyle = HedvigTheme.typography.headlineSmall.fontStyle, + fontSize = HedvigTheme.typography.headlineSmall.fontSize, + fontFamily = HedvigTheme.typography.headlineSmall.fontFamily, + modifier = Modifier.padding(contentPadding), + ) + Spacer(Modifier.height(12.dp)) + LazyRow( + modifier = Modifier.blockSwipeBackOnIos(), + horizontalArrangement = Arrangement.spacedBy(24.dp), + contentPadding = contentPadding, + ) { + val size = 148.dp + items(stories) { story -> + ArticleItem( + story = story, + onNavigateToArticle = onNavigateToArticle, + imageLoader = imageLoader, + size = size, + ) + } + } + Spacer(Modifier.height(48.dp)) + } +} + +@Composable +private fun ArticleItem( + story: PuppyGuideStory, + onNavigateToArticle: (PuppyGuideStory) -> Unit, + imageLoader: ImageLoader, + size: Dp, + modifier: Modifier = Modifier, + shape: Shape = HedvigTheme.shapes.cornerMedium, +) { + val interactionSource = remember { MutableInteractionSource() } + val isRead = story.isRead || story.rating != null + val readAudioLabel = stringResource(Res.string.A11Y_READ_LABEL_HACK) + val audioDescription = "${story.title}, ${story.subtitle}. ${if (isRead) readAudioLabel else ""}." + Column( + modifier + .clearAndSetSemantics { + contentDescription = audioDescription + role = Role.Button + onClick(label = null) { + onNavigateToArticle(story) + true + } + } + .width(size) + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = { + onNavigateToArticle(story) + }, + ), + ) { + Box( + modifier = Modifier + .clip(shape) + .indication(interactionSource, LocalIndication.current), + contentAlignment = Alignment.TopEnd, + ) { + val fallbackPainter: Painter = ColorPainter(Color.Black.copy(alpha = 0.7f)) + AsyncImage( + model = story.image, + contentDescription = EmptyContentDescription, + placeholder = fallbackPainter, + error = fallbackPainter, + fallback = fallbackPainter, + imageLoader = imageLoader, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(size) + .clip(shape), + ) + if (isRead) { + Box( + modifier = Modifier + .matchParentSize() + .background(Color.Black.copy(alpha = 0.4f)), + ) + ReadLabel(modifier = Modifier.padding(12.dp)) + } + } + + Spacer(Modifier.height(8.dp)) + HedvigText( + story.title, + style = HedvigTheme.typography.label, + ) + HedvigText( + story.subtitle, + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + ) + } +} + +@Composable +private fun ReadLabel(modifier: Modifier = Modifier) { + Surface( + modifier = modifier.semantics { + hideFromAccessibility() + }, + shape = HedvigTheme.shapes.cornerXSmall, + color = HedvigTheme.colorScheme.buttonSecondaryAltResting, + ) { + Row( + Modifier.padding( + start = 8.dp, + end = 8.dp, + top = 3.dp, + bottom = 3.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + HedvigText( + text = stringResource(Res.string.PUPPY_GUIDE_LABEL_READ), + textAlign = TextAlign.Center, + style = HedvigTheme.typography.label, + ) + Icon( + HedvigIcons.Checkmark, + EmptyContentDescription, + Modifier.size(20.dp), + ) + } + } +} + +@HedvigShortMultiScreenPreview +@Composable +private fun PuppyArticleScreenAnimations( + @PreviewParameter(PuppyGuideUiStatePreviewProvider::class) uiState: PuppyGuideUiState, +) { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + PuppyGuideScreen( + uiState, + {}, + {}, + reload = {}, + imageLoader = rememberPreviewImageLoader(), + onScrollOffsetChanged = {}, + ) + } + } +} + +@HedvigShortMultiScreenPreview +@Composable +private fun ReadLabelPreview( + @PreviewParameter(PuppyGuideUiStatePreviewProvider::class) uiState: PuppyGuideUiState, +) { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + ReadLabel() + } + } +} + +private const val CategoriesStickyHeaderKey = "puppyGuideCategoriesStickyHeader" + +private class PuppyGuideUiStatePreviewProvider : + CollectionPreviewParameterProvider( + listOf( + PuppyGuideUiState.Success( + stories = listOf( + PuppyGuideStory( + categories = listOf("Food"), + content = "some long long long long long long long long long long long long" + + " long long long long long long long long long long long long content", + image = "", + name = "", + rating = 5, + isRead = true, + subtitle = "5 min read", + title = "Puppy food food food food food food food ", + ), + PuppyGuideStory( + categories = listOf("Training"), + content = "some long long long long long long long long long long long long" + + " long long long long long long long long long long long long content", + image = "", + name = "", + rating = 5, + isRead = false, + subtitle = "4 min read", + title = "Puppy training", + ), + ), + ), + PuppyGuideUiState.Loading, + PuppyGuideUiState.Failure, + ), + ) diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt new file mode 100644 index 0000000000..9be8702273 --- /dev/null +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt @@ -0,0 +1,62 @@ +package com.hedvig.android.feature.help.center.puppyguide + +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.help.center.data.GetPuppyGuideUseCase +import com.hedvig.android.feature.help.center.data.PuppyGuideStory +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope +import com.hedvig.android.molecule.public.MoleculeViewModel +import kotlinx.coroutines.flow.SharingStarted + +internal class PuppyGuideViewModel( + getPuppyGuideUseCase: GetPuppyGuideUseCase, +) : MoleculeViewModel( + presenter = PuppyGuidePresenter(getPuppyGuideUseCase), + initialState = PuppyGuideUiState.Loading, + sharingStarted = SharingStarted.WhileSubscribed(), + ) + +private class PuppyGuidePresenter( + private val getPuppyGuideUseCase: GetPuppyGuideUseCase, +) : MoleculePresenter { + @Composable + override fun MoleculePresenterScope.present(lastState: PuppyGuideUiState): PuppyGuideUiState { + var currentState by remember { mutableStateOf(lastState) } + var loadIteration by remember { mutableIntStateOf(0) } + + CollectEvents { event -> + when (event) { + PuppyGuideEvent.Reload -> loadIteration++ + } + } + + LaunchedEffect(loadIteration) { + getPuppyGuideUseCase.invoke().collect { response -> + currentState = response.fold( + ifLeft = { PuppyGuideUiState.Failure }, + ifRight = { puppyGuide -> PuppyGuideUiState.Success(puppyGuide.stories) }, + ) + } + } + + return currentState + } +} + +internal sealed interface PuppyGuideEvent { + data object Reload : PuppyGuideEvent +} + +internal sealed interface PuppyGuideUiState { + data class Success(val stories: List) : PuppyGuideUiState + + data object Loading : PuppyGuideUiState + + data object Failure : PuppyGuideUiState +} diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyScaffold.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyScaffold.kt new file mode 100644 index 0000000000..a45e9be1bf --- /dev/null +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyScaffold.kt @@ -0,0 +1,56 @@ +package com.hedvig.android.feature.help.center.puppyguide + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.MutableWindowInsets +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.exclude +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.windowInsetsTopHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.Surface + +// Uses [PuppyTopAppBar] so that on iOS — where the top bar is rendered natively — we don't end up +// with the native back button stacked on top of a Compose-rendered one. +@OptIn(ExperimentalLayoutApi::class) +@Composable +internal fun PuppyScaffold(navigateUp: () -> Unit, content: @Composable ColumnScope.() -> Unit) { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + Column(Modifier.fillMaxSize()) { + val consumedWindowInsets = remember { MutableWindowInsets() } + PuppyTopAppBar( + title = "", + onBack = navigateUp, + modifier = Modifier.onSizeChanged { + consumedWindowInsets.insets = WindowInsets(top = it.height) + }, + ) + Column( + modifier = Modifier + .fillMaxSize() + .consumeWindowInsets(consumedWindowInsets) + .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)), + ) { + Spacer( + modifier = Modifier.windowInsetsTopHeight( + WindowInsets.safeDrawing.exclude(consumedWindowInsets).only(WindowInsetsSides.Top), + ), + ) + content() + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) + } + } + } +} diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyTopAppBar.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyTopAppBar.kt new file mode 100644 index 0000000000..4d0f6b165b --- /dev/null +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyTopAppBar.kt @@ -0,0 +1,7 @@ +package com.hedvig.android.feature.help.center.puppyguide + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +internal expect fun PuppyTopAppBar(title: String, onBack: () -> Unit, modifier: Modifier = Modifier) diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/question/HelpCenterQuestionDestination.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/question/HelpCenterQuestionDestination.kt similarity index 95% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/question/HelpCenterQuestionDestination.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/question/HelpCenterQuestionDestination.kt index a24ce56fc9..512d58ee6a 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/question/HelpCenterQuestionDestination.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/question/HelpCenterQuestionDestination.kt @@ -17,13 +17,11 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration 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 androidx.lifecycle.compose.dropUnlessResumed -import com.halilibo.richtext.commonmark.Markdown import com.hedvig.android.compose.ui.plus import com.hedvig.android.design.system.hedvig.HedvigErrorSection import com.hedvig.android.design.system.hedvig.HedvigFullScreenCenterAlignedProgress @@ -33,7 +31,6 @@ import com.hedvig.android.design.system.hedvig.HedvigTheme import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightColor import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightShade.LIGHT import com.hedvig.android.design.system.hedvig.ProvideTextStyle -import com.hedvig.android.design.system.hedvig.RichText import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.design.system.hedvig.TopAppBarWithBack import com.hedvig.android.feature.help.center.ShowNavigateToInboxViewModel @@ -44,6 +41,7 @@ import com.hedvig.android.feature.help.center.question.HelpCenterQuestionUiState import com.hedvig.android.feature.help.center.question.HelpCenterQuestionUiState.NoQuestionFound import com.hedvig.android.feature.help.center.question.HelpCenterQuestionUiState.Success import com.hedvig.android.feature.help.center.ui.HelpCenterSection +import com.hedvig.android.feature.help.center.ui.MarkdownText import com.hedvig.android.feature.help.center.ui.StillNeedHelpSection import hedvig.resources.GENERAL_ERROR_BODY import hedvig.resources.GENERAL_RETRY @@ -93,7 +91,7 @@ private fun HelpCenterQuestionScreen( title = stringResource(Res.string.HC_TITLE), onClick = onNavigateUp, ) - when (val state = uiState) { + when (uiState) { Failure -> { FailureScreen( onClick = onReload, @@ -116,7 +114,7 @@ private fun HelpCenterQuestionScreen( is Success -> { HelpCenterQuestionScreen( - faqItem = state.faqItem, + faqItem = uiState.faqItem, showNavigateToInboxButton = showNavigateToInboxButton, onNavigateToInbox = onNavigateToInbox, onNavigateToNewConversation = onNavigateToNewConversation, @@ -153,7 +151,6 @@ private fun HelpCenterQuestionScreen( onNavigateToInbox: () -> Unit, onNavigateToNewConversation: () -> Unit, ) { - LocalConfiguration.current Column( Modifier .fillMaxSize() @@ -185,11 +182,9 @@ private fun HelpCenterQuestionScreen( ProvideTextStyle( HedvigTheme.typography.bodySmall.copy(color = HedvigTheme.colorScheme.textSecondary), ) { - RichText { - Markdown( - content = faqItem.answer, - ) - } + MarkdownText( + markdown = faqItem.answer, + ) } }, ) diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/question/HelpCenterQuestionViewModel.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/question/HelpCenterQuestionViewModel.kt similarity index 100% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/question/HelpCenterQuestionViewModel.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/question/HelpCenterQuestionViewModel.kt diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/topic/HelpCenterTopicDestination.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/topic/HelpCenterTopicDestination.kt similarity index 99% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/topic/HelpCenterTopicDestination.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/topic/HelpCenterTopicDestination.kt index 82a1bc8f79..dfa18f810f 100644 --- a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/topic/HelpCenterTopicDestination.kt +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/topic/HelpCenterTopicDestination.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp @@ -180,7 +179,6 @@ private fun HelpCenterTopicScreen( ), ) } else { - LocalConfiguration.current Column( modifier = Modifier .fillMaxSize() diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/topic/HelpCenterTopicViewModel.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/topic/HelpCenterTopicViewModel.kt similarity index 100% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/topic/HelpCenterTopicViewModel.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/topic/HelpCenterTopicViewModel.kt diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/ui/HelpCenterSection.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/ui/HelpCenterSection.kt similarity index 100% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/ui/HelpCenterSection.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/ui/HelpCenterSection.kt diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/ui/HelpCenterSectionWithClickableRows.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/ui/HelpCenterSectionWithClickableRows.kt similarity index 100% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/ui/HelpCenterSectionWithClickableRows.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/ui/HelpCenterSectionWithClickableRows.kt diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/ui/MarkdownText.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/ui/MarkdownText.kt new file mode 100644 index 0000000000..dcdaff31c5 --- /dev/null +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/ui/MarkdownText.kt @@ -0,0 +1,136 @@ +package com.hedvig.android.feature.help.center.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.mikepenz.markdown.compose.Markdown +import com.mikepenz.markdown.model.MarkdownColors +import com.mikepenz.markdown.model.MarkdownPadding +import com.mikepenz.markdown.model.MarkdownTypography + +@Composable +fun MarkdownText(markdown: String, modifier: Modifier = Modifier, withArticleStyle: Boolean = false) { + val colors = HedvigTheme.colorScheme + val typography = HedvigTheme.typography + val headingColor = colors.textPrimary + + val markdownColors = object : MarkdownColors { + override val text: Color = colors.textSecondaryTranslucent + override val codeBackground: Color = colors.surfaceSecondary + override val inlineCodeBackground: Color = colors.surfaceSecondary + override val dividerColor: Color = colors.borderPrimary + override val tableBackground: Color = colors.surfaceSecondary + } + + val markdownTypography = if (withArticleStyle) { + object : MarkdownTypography { + override val h1: TextStyle = typography.bodySmall.copy( + color = headingColor, + fontWeight = FontWeight.Normal, + ) + override val h2: TextStyle = typography.bodySmall.copy( + color = headingColor, + fontWeight = FontWeight.Normal, + ) + override val h3: TextStyle = typography.bodySmall.copy( + color = headingColor, + fontWeight = FontWeight.Normal, + ) + override val h4: TextStyle = typography.bodySmall.copy( + color = headingColor, + fontWeight = FontWeight.Normal, + ) + override val h5: TextStyle = typography.bodySmall.copy( + color = headingColor, + fontWeight = FontWeight.Normal, + ) + override val h6: TextStyle = typography.bodySmall.copy( + color = headingColor, + fontWeight = FontWeight.Normal, + ) + override val text: TextStyle = typography.bodySmall.copy( + color = colors.textSecondaryTranslucent, + fontWeight = FontWeight.Normal, + ) + override val paragraph: TextStyle = typography.bodySmall.copy( + color = colors.textSecondaryTranslucent, + fontWeight = FontWeight.Normal, + ) + override val code: TextStyle = typography.label + override val inlineCode: TextStyle = typography.label + override val bullet: TextStyle = typography.bodySmall.copy( + color = colors.textSecondaryTranslucent, + ) + override val list: TextStyle = typography.bodySmall.copy( + color = colors.textSecondaryTranslucent, + ) + override val ordered: TextStyle = typography.bodySmall.copy( + color = colors.textSecondaryTranslucent, + ) + override val quote: TextStyle = typography.bodySmall.copy( + color = colors.textSecondaryTranslucent, + ) + override val table: TextStyle = typography.bodySmall.copy( + color = colors.textSecondaryTranslucent, + ) + override val textLink = TextLinkStyles( + style = SpanStyle(color = colors.link, textDecoration = TextDecoration.Underline), + focusedStyle = SpanStyle(color = colors.link, textDecoration = TextDecoration.Underline), + hoveredStyle = SpanStyle(color = colors.link, textDecoration = TextDecoration.Underline), + pressedStyle = SpanStyle(color = colors.link, textDecoration = TextDecoration.Underline), + ) + } + } else { + object : MarkdownTypography { + override val h1: TextStyle = typography.headlineLarge + override val h2: TextStyle = typography.headlineMedium + override val h3: TextStyle = typography.headlineSmall + override val h4: TextStyle = typography.displaySmall + override val h5: TextStyle = typography.bodyLarge + override val h6: TextStyle = typography.bodyMedium + override val text: TextStyle = typography.bodySmall + override val paragraph: TextStyle = typography.bodySmall + override val code: TextStyle = typography.label + override val inlineCode: TextStyle = typography.label + override val bullet: TextStyle = typography.bodySmall + override val list: TextStyle = typography.bodySmall + override val ordered: TextStyle = typography.bodySmall + override val quote: TextStyle = typography.bodySmall + override val table: TextStyle = typography.bodySmall + override val textLink = TextLinkStyles( + style = SpanStyle(color = colors.link, textDecoration = TextDecoration.Underline), + focusedStyle = SpanStyle(color = colors.link, textDecoration = TextDecoration.Underline), + hoveredStyle = SpanStyle(color = colors.link, textDecoration = TextDecoration.Underline), + pressedStyle = SpanStyle(color = colors.link, textDecoration = TextDecoration.Underline), + ) + } + } + + Markdown( + content = markdown, + modifier = modifier, + colors = markdownColors, + typography = markdownTypography, + padding = object : MarkdownPadding { + override val block: Dp = 0.dp + override val blockQuote: PaddingValues = PaddingValues(0.dp) + override val blockQuoteBar: PaddingValues.Absolute = PaddingValues.Absolute(0.dp) + override val blockQuoteText: PaddingValues = PaddingValues(0.dp) + override val codeBlock: PaddingValues = PaddingValues(0.dp) + override val list: Dp = 0.dp + override val listIndent: Dp = 0.dp + override val listItemBottom: Dp = 0.dp + override val listItemTop: Dp = 0.dp + }, + ) +} diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/ui/RatingHaptic.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/ui/RatingHaptic.kt new file mode 100644 index 0000000000..2bb7abf3e7 --- /dev/null +++ b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/ui/RatingHaptic.kt @@ -0,0 +1,14 @@ +package com.hedvig.android.feature.help.center.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.hapticfeedback.HapticFeedback + +/** + * Returns a function that plays a haptic for the given article rating (1..5). + * + * Compose Multiplatform's iOS [androidx.compose.ui.hapticfeedback.HapticFeedback] maps most "subtle" + * [androidx.compose.ui.hapticfeedback.HapticFeedbackType] values to UISelectionFeedbackGenerator, + * which is imperceptible. This abstraction bypasses that on iOS by calling UIKit generators directly. + */ +@Composable +expect fun rememberPerformRatingHaptic(hapticFeedback: HapticFeedback): (rating: Int) -> Unit diff --git a/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/ui/StillNeedHelpSection.kt b/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/ui/StillNeedHelpSection.kt similarity index 100% rename from app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/ui/StillNeedHelpSection.kt rename to app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/ui/StillNeedHelpSection.kt diff --git a/app/feature/feature-help-center/src/commonMain/resources/drawable/hundar_badar_pet.jpg b/app/feature/feature-help-center/src/commonMain/resources/drawable/hundar_badar_pet.jpg new file mode 100644 index 0000000000..c7e8f8bfc2 Binary files /dev/null and b/app/feature/feature-help-center/src/commonMain/resources/drawable/hundar_badar_pet.jpg differ diff --git a/app/feature/feature-help-center/src/jvmAndAndroidMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyTopAppBar.kt b/app/feature/feature-help-center/src/jvmAndAndroidMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyTopAppBar.kt new file mode 100644 index 0000000000..ce2fdc5eb0 --- /dev/null +++ b/app/feature/feature-help-center/src/jvmAndAndroidMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyTopAppBar.kt @@ -0,0 +1,10 @@ +package com.hedvig.android.feature.help.center.puppyguide + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.hedvig.android.design.system.hedvig.TopAppBarWithBack + +@Composable +internal actual fun PuppyTopAppBar(title: String, onBack: () -> Unit, modifier: Modifier) { + TopAppBarWithBack(title = title, onClick = onBack, modifier = modifier) +} diff --git a/app/feature/feature-help-center/src/jvmAndAndroidMain/kotlin/com/hedvig/android/feature/help/center/ui/RatingHaptic.kt b/app/feature/feature-help-center/src/jvmAndAndroidMain/kotlin/com/hedvig/android/feature/help/center/ui/RatingHaptic.kt new file mode 100644 index 0000000000..737e841caa --- /dev/null +++ b/app/feature/feature-help-center/src/jvmAndAndroidMain/kotlin/com/hedvig/android/feature/help/center/ui/RatingHaptic.kt @@ -0,0 +1,19 @@ +package com.hedvig.android.feature.help.center.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.hapticfeedback.HapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType + +@Composable +actual fun rememberPerformRatingHaptic(hapticFeedback: HapticFeedback): (rating: Int) -> Unit { + return remember(hapticFeedback) { + { rating -> + val type = when (rating) { + 4, 5 -> HapticFeedbackType.Confirm + else -> HapticFeedbackType.TextHandleMove + } + hapticFeedback.performHapticFeedback(type) + } + } +} diff --git a/app/feature/feature-help-center/src/main/AndroidManifest.xml b/app/feature/feature-help-center/src/main/AndroidManifest.xml deleted file mode 100644 index 568741e54f..0000000000 --- a/app/feature/feature-help-center/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/app/feature/feature-help-center/src/nativeMain/kotlin/com/hedvig/android/feature/help/center/data/PuppyGuideAvailability.kt b/app/feature/feature-help-center/src/nativeMain/kotlin/com/hedvig/android/feature/help/center/data/PuppyGuideAvailability.kt new file mode 100644 index 0000000000..debe85a433 --- /dev/null +++ b/app/feature/feature-help-center/src/nativeMain/kotlin/com/hedvig/android/feature/help/center/data/PuppyGuideAvailability.kt @@ -0,0 +1,36 @@ +package com.hedvig.android.feature.help.center.data + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.koin.mp.KoinPlatform + +@Suppress("unused") // Used from iOS +fun observePuppyGuideAvailability(onResult: (PuppyGuidePresentation?) -> Unit): PuppyGuideAvailabilityCancellable { + val useCase = KoinPlatform.getKoin().get() + val scope = CoroutineScope(Dispatchers.Main) + val job: Job = scope.launch { + useCase.invoke().collectLatest { either -> + val puppyGuide = either.getOrNull() + val presentation = when { + puppyGuide == null || puppyGuide.stories.isEmpty() -> null + puppyGuide.isForYoungDog == true -> PuppyGuidePresentation.FullCard + else -> PuppyGuidePresentation.QuickAction + } + onResult(presentation) + } + } + return PuppyGuideAvailabilityCancellable { job.cancel() } +} + +sealed class PuppyGuidePresentation { + object FullCard : PuppyGuidePresentation() + + object QuickAction : PuppyGuidePresentation() +} + +class PuppyGuideAvailabilityCancellable internal constructor(private val onCancel: () -> Unit) { + fun cancel() = onCancel() +} diff --git a/app/feature/feature-help-center/src/nativeMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyTopAppBar.kt b/app/feature/feature-help-center/src/nativeMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyTopAppBar.kt new file mode 100644 index 0000000000..078c267729 --- /dev/null +++ b/app/feature/feature-help-center/src/nativeMain/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyTopAppBar.kt @@ -0,0 +1,11 @@ +package com.hedvig.android.feature.help.center.puppyguide + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +// The top app bar on iOS is implemented natively, so this is rendered as empty spacing. +@Composable +internal actual fun PuppyTopAppBar(title: String, onBack: () -> Unit, modifier: Modifier) { + Spacer(modifier) +} diff --git a/app/feature/feature-help-center/src/nativeMain/kotlin/com/hedvig/android/feature/help/center/ui/PuppyGuideViewControllers.kt b/app/feature/feature-help-center/src/nativeMain/kotlin/com/hedvig/android/feature/help/center/ui/PuppyGuideViewControllers.kt new file mode 100644 index 0000000000..3e95454588 --- /dev/null +++ b/app/feature/feature-help-center/src/nativeMain/kotlin/com/hedvig/android/feature/help/center/ui/PuppyGuideViewControllers.kt @@ -0,0 +1,52 @@ +package com.hedvig.android.feature.help.center.ui + +import coil3.ImageLoader +import com.hedvig.android.design.system.hedvig.HedvigComposeUIViewController +import com.hedvig.android.design.system.hedvig.api.IosSwipeBackController +import com.hedvig.android.feature.help.center.puppyguide.PuppyArticleDestination +import com.hedvig.android.feature.help.center.puppyguide.PuppyArticleViewModel +import com.hedvig.android.feature.help.center.puppyguide.PuppyGuideDestination +import com.hedvig.android.feature.help.center.puppyguide.PuppyGuideViewModel +import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.parameter.parametersOf +import platform.UIKit.UIViewController + +@Suppress("unused", "FunctionName") // Used from iOS +fun PuppyGuideViewController( + onNavigateUp: () -> Unit, + onNavigateToArticle: (storyName: String) -> Unit, + swipeBackController: IosSwipeBackController, + onScrollOffsetChanged: (Float) -> Unit, +): UIViewController { + return HedvigComposeUIViewController(swipeBackController) { + val imageLoader = koinInject() + val viewModel = koinViewModel() + PuppyGuideDestination( + viewModel = viewModel, + onNavigateUp = onNavigateUp, + imageLoader = imageLoader, + onNavigateToArticle = { story -> onNavigateToArticle(story.name) }, + onScrollOffsetChanged = onScrollOffsetChanged, + ) + } +} + +@Suppress("unused", "FunctionName") // Used from iOS +fun PuppyArticleViewController( + storyName: String, + navigateUp: () -> Unit, + swipeBackController: IosSwipeBackController, + onScrollOffsetChanged: (Float) -> Unit, +): UIViewController { + return HedvigComposeUIViewController(swipeBackController) { + val imageLoader = koinInject() + val viewModel = koinViewModel { parametersOf(storyName) } + PuppyArticleDestination( + viewModel = viewModel, + navigateUp = navigateUp, + imageLoader = imageLoader, + onScrollOffsetChanged = onScrollOffsetChanged, + ) + } +} diff --git a/app/feature/feature-help-center/src/nativeMain/kotlin/com/hedvig/android/feature/help/center/ui/RatingHaptic.native.kt b/app/feature/feature-help-center/src/nativeMain/kotlin/com/hedvig/android/feature/help/center/ui/RatingHaptic.native.kt new file mode 100644 index 0000000000..007187e97d --- /dev/null +++ b/app/feature/feature-help-center/src/nativeMain/kotlin/com/hedvig/android/feature/help/center/ui/RatingHaptic.native.kt @@ -0,0 +1,40 @@ +package com.hedvig.android.feature.help.center.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.hapticfeedback.HapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import platform.UIKit.UIImpactFeedbackGenerator +import platform.UIKit.UIImpactFeedbackStyle + +/** + * `Confirm` and `LongPress` are delegated to Compose's iOS impl (they map to + * `UINotificationFeedbackGenerator.success` and a medium `UIImpactFeedbackGenerator` respectively). + * + * The "subtle" 1-3 case and the extra thump on 5 use UIKit directly because Compose's iOS impl + * routes every other "light" `HapticFeedbackType` to `UISelectionFeedbackGenerator` + * (imperceptible) and never exposes `UIImpactFeedbackStyleLight`/`Heavy`. + */ +@Composable +actual fun rememberPerformRatingHaptic(hapticFeedback: HapticFeedback): (rating: Int) -> Unit { + return remember(hapticFeedback) { + val lightImpact = UIImpactFeedbackGenerator(style = UIImpactFeedbackStyle.UIImpactFeedbackStyleLight) + val heavyImpact = UIImpactFeedbackGenerator(style = UIImpactFeedbackStyle.UIImpactFeedbackStyleHeavy); + { rating: Int -> + when (rating) { + 4 -> { + hapticFeedback.performHapticFeedback(HapticFeedbackType.Confirm) + } + + 5 -> { + hapticFeedback.performHapticFeedback(HapticFeedbackType.Confirm) + heavyImpact.impactOccurred() + } + + else -> { + lightImpact.impactOccurred() + } + } + } + } +} diff --git a/app/feature/feature-help-center/src/test/kotlin/GetQuickLinksUseCaseTest.kt b/app/feature/feature-help-center/src/test/kotlin/GetQuickLinksUseCaseTest.kt index 2d19f1bf7b..526307cd33 100644 --- a/app/feature/feature-help-center/src/test/kotlin/GetQuickLinksUseCaseTest.kt +++ b/app/feature/feature-help-center/src/test/kotlin/GetQuickLinksUseCaseTest.kt @@ -11,7 +11,7 @@ import com.hedvig.android.apollo.test.TestNetworkTransportType import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.test.isRight import com.hedvig.android.feature.help.center.data.GetMemberActionsUseCase -import com.hedvig.android.feature.help.center.data.GetQuickLinksUseCase +import com.hedvig.android.feature.help.center.data.GetQuickLinksUseCaseImpl import com.hedvig.android.feature.help.center.data.MemberAction import com.hedvig.android.feature.help.center.data.QuickLinkDestination import com.hedvig.android.feature.help.center.model.QuickAction @@ -67,7 +67,7 @@ class GetQuickLinksUseCaseTest { val featureManager = FakeFeatureManager(fixedReturnForAll = true) val getMemberActionsUseCase = FakeGetMemberActionsUseCase() getMemberActionsUseCase.turbine.add(fakeMemberActionWithTier.right()) - val useCase = GetQuickLinksUseCase( + val useCase = GetQuickLinksUseCaseImpl( apolloClient = apolloClientWithGoodResponse, featureManager = featureManager, getMemberActionsUseCase = getMemberActionsUseCase, @@ -98,7 +98,7 @@ class GetQuickLinksUseCaseTest { val featureManager = FakeFeatureManager(fixedReturnForAll = true) val getMemberActionsUseCase = FakeGetMemberActionsUseCase() getMemberActionsUseCase.turbine.add(fakeMemberActionWithoutTier.right()) - val useCase = GetQuickLinksUseCase( + val useCase = GetQuickLinksUseCaseImpl( apolloClient = apolloClientWithGoodResponse, featureManager = featureManager, getMemberActionsUseCase = getMemberActionsUseCase, diff --git a/app/feature/feature-home/build.gradle.kts b/app/feature/feature-home/build.gradle.kts index 5554784f82..671e4f1567 100644 --- a/app/feature/feature-home/build.gradle.kts +++ b/app/feature/feature-home/build.gradle.kts @@ -45,7 +45,7 @@ dependencies { implementation(projects.dataConversations) implementation(projects.dataCrossSellAfterClaimClosed) implementation(projects.designSystemHedvig) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.languageCore) implementation(projects.memberRemindersPublic) implementation(projects.memberRemindersUi) 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 4aa80cf09e..bed682b0bb 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 @@ -78,6 +78,7 @@ fun NavGraphBuilder.homeGraph( sections, navigateUp = navController::navigateUp, navigateBack = navController::popBackStack, + openUrl = openUrl, ) } nestedGraphs() diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/FirstVetDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/FirstVetDestination.kt index 0ff43878cb..9480c830af 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/FirstVetDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/FirstVetDestination.kt @@ -5,10 +5,16 @@ import com.hedvig.android.ui.emergency.FirstVetScreen import com.hedvig.android.ui.emergency.FirstVetSection @Composable -internal fun FirstVetDestination(sections: List, navigateUp: () -> Unit, navigateBack: () -> Unit) { +internal fun FirstVetDestination( + sections: List, + navigateUp: () -> Unit, + navigateBack: () -> Unit, + openUrl: (String) -> Unit, +) { FirstVetScreen( sections = sections, navigateUp = navigateUp, navigateBack = navigateBack, + openUrl = openUrl, ) } diff --git a/app/feature/feature-image-viewer/src/main/kotlin/com/hedvig/android/feature/imageviewer/ImageViewerDestination.kt b/app/feature/feature-image-viewer/src/main/kotlin/com/hedvig/android/feature/imageviewer/ImageViewerDestination.kt index 51453d5622..781b590c8b 100644 --- a/app/feature/feature-image-viewer/src/main/kotlin/com/hedvig/android/feature/imageviewer/ImageViewerDestination.kt +++ b/app/feature/feature-image-viewer/src/main/kotlin/com/hedvig/android/feature/imageviewer/ImageViewerDestination.kt @@ -1,5 +1,7 @@ package com.hedvig.android.feature.imageviewer +import android.graphics.Canvas as AndroidCanvas +import android.graphics.Color as AndroidColor import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -19,19 +21,19 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import android.graphics.Canvas as AndroidCanvas -import android.graphics.Color as AndroidColor import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import androidx.core.graphics.createBitmap import androidx.lifecycle.compose.dropUnlessResumed import coil3.Bitmap import coil3.ImageLoader import coil3.compose.AsyncImage import coil3.request.ImageRequest +import coil3.request.transformations import coil3.size.Size import coil3.transform.Transformation import com.hedvig.android.design.system.hedvig.HedvigPreview @@ -51,8 +53,6 @@ import hedvig.resources.TALKBACK_OPEN_EXTERNAL_LINK import hedvig.resources.TALKBACK_PINCH_TO_ZOOM import hedvig.resources.general_back_button import org.jetbrains.compose.resources.stringResource -import androidx.core.graphics.createBitmap -import coil3.request.transformations @Composable internal fun ImageViewerDestination( @@ -63,8 +63,8 @@ internal fun ImageViewerDestination( ) { Box( Modifier - .fillMaxSize() - .background(Color.Black), + .fillMaxSize() + .background(Color.Black), ) { val zoomableState = rememberZoomableState(overZoomConfig = OverZoomConfig(1f, 4f)) Zoomable( @@ -81,10 +81,10 @@ internal fun ImageViewerDestination( .build(), contentDescription = stringResource(Res.string.TALKBACK_PINCH_TO_ZOOM), modifier = Modifier - .fillMaxSize() - .semantics { - role = Role.Image - }, + .fillMaxSize() + .semantics { + role = Role.Image + }, ) } HedvigTheme(darkTheme = true) { @@ -95,10 +95,12 @@ internal fun ImageViewerDestination( Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)) - .padding(horizontal = 4.dp) - .fillMaxWidth() - .height(64.dp), + .windowInsetsPadding( + WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top), + ) + .padding(horizontal = 4.dp) + .fillMaxWidth() + .height(64.dp), ) { val description = stringResource(Res.string.general_back_button) IconButton( @@ -141,7 +143,6 @@ class WhiteBackgroundTransformation : Transformation() { } } - @HedvigPreview @Composable private fun PreviewImageViewerDestination() { diff --git a/app/feature/feature-insurances/build.gradle.kts b/app/feature/feature-insurances/build.gradle.kts index 9e71111b52..b95f551ca5 100644 --- a/app/feature/feature-insurances/build.gradle.kts +++ b/app/feature/feature-insurances/build.gradle.kts @@ -38,7 +38,7 @@ dependencies { implementation(projects.dataDisplayItems) implementation(projects.dataProductVariantPublic) implementation(projects.designSystemHedvig) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.languageCore) implementation(projects.moleculePublic) implementation(projects.navigationCommon) diff --git a/app/feature/feature-movingflow/build.gradle.kts b/app/feature/feature-movingflow/build.gradle.kts index 8d58185e6b..4e737e559a 100644 --- a/app/feature/feature-movingflow/build.gradle.kts +++ b/app/feature/feature-movingflow/build.gradle.kts @@ -17,8 +17,6 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.apollo.normalizedCache) implementation(libs.arrow.core) - implementation(libs.compose.richtext) - implementation(libs.compose.richtextCommonmark) implementation(libs.coroutines.core) implementation(libs.jetbrains.lifecycle.runtime.compose) implementation(libs.koin.composeViewModel) @@ -35,7 +33,7 @@ dependencies { implementation(projects.dataCrossSellAfterFlow) implementation(projects.dataProductVariantPublic) implementation(projects.designSystemHedvig) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.moleculePublic) implementation(projects.navigationCommon) implementation(projects.navigationCompose) diff --git a/app/feature/feature-payments/build.gradle.kts b/app/feature/feature-payments/build.gradle.kts index 427d2fca10..83a8164ee8 100644 --- a/app/feature/feature-payments/build.gradle.kts +++ b/app/feature/feature-payments/build.gradle.kts @@ -10,6 +10,7 @@ hedvig { } dependencies { + implementation(libs.androidx.compose.foundation) implementation(libs.apollo.normalizedCache) implementation(libs.apollo.runtime) implementation(libs.arrow.core) @@ -22,22 +23,41 @@ dependencies { 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.featureFlags) 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) } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt index 7de6f1cde8..1aec65b1c4 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/GetManualChargeInfoUseCase.kt @@ -23,7 +23,6 @@ internal class GetManualChargeInfoUseCaseImpl( private val apolloClient: ApolloClient, ) : GetManualChargeInfoUseCase { override suspend fun invoke(): Either = either { - val currentMember = apolloClient.query(ManualChargeInfoQuery()) .fetchPolicy(FetchPolicy.NetworkOnly) .safeExecute(::ErrorMessage) 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 4ed7d586cf..627df2ddd2 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 @@ -170,7 +170,9 @@ internal fun String?.toChargeMethod(): MemberPaymentChargeMethod { return when { this?.startsWith("kivra", ignoreCase = true) == true || this?.startsWith("invoice", ignoreCase = true) == true -> MemberPaymentChargeMethod.INVOICE + this?.startsWith("trustly", ignoreCase = true) == true -> MemberPaymentChargeMethod.TRUSTLY + else -> MemberPaymentChargeMethod.UNKNOWN } } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt index 7d63db09b1..4dc3196ddf 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/data/TriggerManualChargeUseCase.kt @@ -27,10 +27,14 @@ internal class TriggerManualChargeUseCaseImpl( } .bind() - if (result.manuallyChargeMember.userError != null) raise( - ErrorMessage( - result.manuallyChargeMember.userError.message, - ), - ) else Unit + if (result.manuallyChargeMember.userError != null) { + raise( + ErrorMessage( + result.manuallyChargeMember.userError.message, + ), + ) + } else { + Unit + } } } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt index e6a9028f52..a7e0bd8189 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsDestination.kt @@ -30,11 +30,10 @@ internal sealed interface PaymentsDestinations { data object MemberPaymentDetails : PaymentsDestinations, Destination @Serializable - data object ManualCharge: PaymentsDestinations, Destination + data object ManualCharge : PaymentsDestinations, Destination @Serializable data class ManualChargeSuccess( - val showCancellationWarning: Boolean - ): PaymentsDestinations, Destination + val showCancellationWarning: Boolean, + ) : PaymentsDestinations, Destination } - diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/navigation/PaymentsGraph.kt index 08804dd98b..cd6502e76c 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 @@ -85,22 +85,25 @@ fun NavGraphBuilder.paymentsGraph( ) }, onNavigateToSuccess = { showCancellationWarning -> - navController.navigate(PaymentsDestinations.ManualChargeSuccess( - showCancellationWarning = showCancellationWarning - )) { + navController.navigate( + PaymentsDestinations.ManualChargeSuccess( + showCancellationWarning = showCancellationWarning, + ), + ) { typedPopUpTo { inclusive = true } } }, - openConversation = openConversation + openConversation = openConversation, ) } - navdestination{ + navdestination { ManualChargeSuccessDestination( this.showCancellationWarning, - navController::navigateUp) + navController::navigateUp, + ) } navdestination { diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt index d89ed985e6..2262867ba4 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/overview/data/GetUpcomingPaymentUseCase.kt @@ -36,7 +36,6 @@ internal data class GetUpcomingPaymentUseCaseImpl( val clock: Clock, ) : GetUpcomingPaymentUseCase { override suspend fun invoke(): Either = either { - val result = apolloClient.query(UpcomingPaymentQuery()) .fetchPolicy(FetchPolicy.NetworkFirst) .safeExecute(::ErrorMessage) @@ -53,7 +52,9 @@ internal data class GetUpcomingPaymentUseCaseImpl( } if (failedChargeNet != null) { ManualChargeToPrompt(failedChargeNet) - } else null + } else { + null + } } else { null } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt index 27fcf3c208..c9786597f1 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeDestination.kt @@ -104,16 +104,22 @@ private fun ManualChargeScreen( topAppBarText = stringResource(Res.string.PAYMENTS_PAYMENT_OVERDUE_TITLE), ) { when (uiState) { - is ManualChargeUiState.Failure -> { - val title = if (uiState.error.message != null) - stringResource(Res.string.SELF_MANUAL_CHARGE_CHANGES_BEEN_MADE_TITLE) else + val title = if (uiState.error.message != null) { + stringResource(Res.string.SELF_MANUAL_CHARGE_CHANGES_BEEN_MADE_TITLE) + } else { stringResource(Res.string.something_went_wrong) - val subTitle = if (uiState.error.message != null) uiState.error.message else + } + val subTitle = if (uiState.error.message != null) { + uiState.error.message + } else { stringResource(Res.string.GENERAL_ERROR_BODY) - val buttonText = if (uiState.error.message != null) - stringResource(Res.string.claim_status_detail_chat_button_description) else + } + val buttonText = if (uiState.error.message != null) { + stringResource(Res.string.claim_status_detail_chat_button_description) + } else { stringResource(Res.string.GENERAL_RETRY) + } val onButtonClick = if (uiState.error.message != null) openConversation else reload HedvigErrorSection( @@ -125,7 +131,6 @@ private fun ManualChargeScreen( buttonText = buttonText, title = title, ) - } ManualChargeUiState.Loading -> { @@ -160,8 +165,6 @@ private fun ManualChargeSuccessScreen( val dateTimeFormatter = rememberHedvigMonthDateTimeFormatter() val dateTimeFormatterWithYear = rememberHedvigDateTimeFormatter() Column { - - Column( modifier = Modifier .padding( @@ -181,7 +184,6 @@ private fun ManualChargeSuccessScreen( color = HedvigTheme.colorScheme.borderPrimary, shape = HedvigTheme.shapes.cornerXLarge, ) - .clip(HedvigTheme.shapes.cornerXLarge) .padding(16.dp), ) { @@ -244,8 +246,7 @@ private fun ManualChargeSuccessScreen( style = HedvigTheme.typography.label, ) } - if (uiState.manualChargeInfo.bankDescriptor != null - ) { + if (uiState.manualChargeInfo.bankDescriptor != null) { Spacer(Modifier.height(10.dp)) Row( modifier = Modifier.fillMaxWidth(), @@ -291,7 +292,7 @@ private fun ManualChargeSuccessScreen( enabled = !uiState.payButtonLoading, buttonSize = ButtonDefaults.ButtonSize.Medium, modifier = Modifier.fillMaxWidth(), - isLoading = uiState.payButtonLoading + isLoading = uiState.payButtonLoading, ) Spacer(modifier = Modifier.height(8.dp)) @@ -383,8 +384,12 @@ private fun ManualChargeScreenFailurePreview( ManualChargeScreen( uiState = ManualChargeUiState.Failure( ErrorMessage( - message = if (hasUserError) "Cannot charge the failed payment since there have been some changes. " + - "The new amount will be included in the upcoming payment." else null, + message = if (hasUserError) { + "Cannot charge the failed payment since there have been some changes. " + + "The new amount will be included in the upcoming payment." + } else { + null + }, ), ), navigateUp = {}, @@ -397,5 +402,3 @@ private fun ManualChargeScreenFailurePreview( } } } - - diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt index 75aae2975b..75be169087 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeSuccessDestination.kt @@ -38,10 +38,7 @@ import hedvig.resources.general_close_button import org.jetbrains.compose.resources.stringResource @Composable -internal fun ManualChargeSuccessDestination( - showCancellationWarning: Boolean, - popBackStack: () -> Unit, -) { +internal fun ManualChargeSuccessDestination(showCancellationWarning: Boolean, popBackStack: () -> Unit) { Column( modifier = Modifier .fillMaxSize() @@ -60,9 +57,11 @@ internal fun ManualChargeSuccessDestination( description = stringResource( Res.string.PAYMENTS_PAYMENT_IN_PROGRESS_DESCRIPTION, ), - iconStyle = if (showCancellationWarning) + iconStyle = if (showCancellationWarning) { EmptyStateDefaults.EmptyStateIconStyle.SUCCESS_WITH_WARNING - else SUCCESS, + } else { + SUCCESS + }, buttonStyle = NoButton, ) Column(Modifier.weight(1f)) { @@ -82,7 +81,7 @@ internal fun ManualChargeSuccessDestination( buttonSize = Large, modifier = Modifier.fillMaxWidth(), enabled = true, - buttonStyle = ButtonDefaults.ButtonStyle.Secondary + buttonStyle = ButtonDefaults.ButtonStyle.Secondary, ) Spacer(Modifier.height(16.dp)) } diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt index d1e058343c..029e7dc8b1 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/manualcharge/ManualChargeViewModel.kt @@ -19,26 +19,30 @@ internal class ManualChargeViewModel( getManualChargeInfoUseCase: GetManualChargeInfoUseCase, triggerManualCharge: TriggerManualChargeUseCase, ) : MoleculeViewModel( - initialState = ManualChargeUiState.Loading, - presenter = ManualChargePresenter(getManualChargeInfoUseCase, triggerManualCharge), -) + initialState = ManualChargeUiState.Loading, + presenter = ManualChargePresenter(getManualChargeInfoUseCase, triggerManualCharge), + ) private class ManualChargePresenter( private val getManualChargeInfoUseCase: GetManualChargeInfoUseCase, private val triggerManualCharge: TriggerManualChargeUseCase, ) : MoleculePresenter { @Composable - override fun MoleculePresenterScope.present( - lastState: ManualChargeUiState, - ): ManualChargeUiState { + override fun MoleculePresenterScope.present(lastState: ManualChargeUiState): ManualChargeUiState { var dataLoadIteration by remember { mutableIntStateOf(0) } var screenState by remember { mutableStateOf(lastState) } var triggerChargeIteration by remember { mutableIntStateOf(0) } CollectEvents { when (it) { - ManualChargeEvent.Retry -> dataLoadIteration++ - ManualChargeEvent.TriggerCharge -> triggerChargeIteration++ + ManualChargeEvent.Retry -> { + dataLoadIteration++ + } + + ManualChargeEvent.TriggerCharge -> { + triggerChargeIteration++ + } + ManualChargeEvent.ClearNav -> { val currentState = screenState as? ManualChargeUiState.Success ?: return@CollectEvents screenState = currentState.copy(navigateToSuccess = null) @@ -100,6 +104,6 @@ internal sealed interface ManualChargeEvent { data object Retry : ManualChargeEvent data object TriggerCharge : ManualChargeEvent + data object ClearNav : ManualChargeEvent } - diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsDestination.kt index 1c266595af..a7842273ea 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/memberpaymentdetails/MemberPaymentDetailsDestination.kt @@ -252,22 +252,16 @@ private fun MemberPaymentDetailsSuccessScreen( @Composable private fun PaymentMethod.label(): String = when (this) { PaymentMethod.TRUSTLY -> stringResource(R.string.PAYMENTS_AUTOGIRO_LABEL) - PaymentMethod.NORDEA -> stringResource(R.string.BANK_PAYOUT_METHOD_CARD_TITLE) - PaymentMethod.INVOICE -> stringResource(R.string.PAYMENTS_INVOICE) - PaymentMethod.SWISH -> stringResource(Res.string.swish) } @Composable private fun PaymentAccount.label(): String = when (this) { PaymentAccount.Kivra -> "Kivra" - is PaymentAccount.Email -> email - is PaymentAccount.PhoneNumber -> phoneNumber - is PaymentAccount.BankAccount -> account } 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 915c794056..0c4091d20d 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 @@ -69,8 +69,8 @@ import com.hedvig.android.design.system.hedvig.icon.Card import com.hedvig.android.design.system.hedvig.icon.ChevronRight import com.hedvig.android.design.system.hedvig.icon.Clock import com.hedvig.android.design.system.hedvig.icon.HedvigIcons -import com.hedvig.android.design.system.hedvig.icon.WarningFilled import com.hedvig.android.design.system.hedvig.icon.PaymentOutline +import com.hedvig.android.design.system.hedvig.icon.WarningFilled import com.hedvig.android.design.system.hedvig.placeholder.hedvigPlaceholder import com.hedvig.android.design.system.hedvig.placeholder.shimmer import com.hedvig.android.design.system.hedvig.rememberHedvigDateTimeFormatter @@ -258,7 +258,7 @@ private fun PaymentsContent( FailedPaymentInfo( amountDue = upcomingPaymentInfo.isManualChargeAllowed.sum.toString(), onReviewPaymentClick = onOpenManualCharge, - modifier = Modifier.padding(horizontal = 16.dp) + modifier = Modifier.padding(horizontal = 16.dp), ) Spacer(Modifier.height(8.dp)) } @@ -343,7 +343,7 @@ private fun PaymentsContent( is ConnectedPaymentInfo.NeedsSetup, ConnectedPaymentInfo.Unknown, is ConnectedPaymentInfo.Active, - -> { + -> { } } } @@ -378,10 +378,7 @@ private fun CardNotConnectedWarningCard( } @Composable -private fun UpcomingPaymentInfoCard( - upcomingPaymentInfo: UpcomingPaymentInfo?, - modifier: Modifier = Modifier, -) { +private fun UpcomingPaymentInfoCard(upcomingPaymentInfo: UpcomingPaymentInfo?, modifier: Modifier = Modifier) { Box(modifier) { when (upcomingPaymentInfo) { NoInfo -> {} @@ -625,9 +622,12 @@ private fun FailedPaymentInfo(amountDue: String, onReviewPaymentClick: () -> Uni color = HedvigTheme.colorScheme.fillNegative, modifier = modifier .fillMaxWidth() - .border(1.dp, HedvigTheme.colorScheme.borderPrimary, - HedvigTheme.shapes.cornerXLarge) - .hedvigDropShadow() + .border( + 1.dp, + HedvigTheme.colorScheme.borderPrimary, + HedvigTheme.shapes.cornerXLarge, + ) + .hedvigDropShadow(), ) { Column( modifier = Modifier @@ -733,7 +733,6 @@ private fun PreviewFailedPaymentInfo() { } } - @Composable @HedvigPreview private fun PreviewPaymentScreen( 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 3e0ede1f47..c7a403a282 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 @@ -13,8 +13,8 @@ import com.hedvig.android.core.demomode.Provider import com.hedvig.android.core.uidata.UiMoney import com.hedvig.android.feature.payments.data.ManualChargeToPrompt import com.hedvig.android.feature.payments.data.MemberCharge -import com.hedvig.android.feature.payments.data.PaymentConnection import com.hedvig.android.feature.payments.data.MemberPaymentChargeMethod +import com.hedvig.android.feature.payments.data.PaymentConnection 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 @@ -26,9 +26,9 @@ import com.hedvig.android.feature.payments.overview.data.GetUpcomingPaymentUseCa import com.hedvig.android.feature.payments.ui.payments.PaymentsUiState.Content.ConnectedPaymentInfo import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope -import kotlinx.coroutines.flow.collectLatest import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.firstOrNull import kotlinx.datetime.LocalDate diff --git a/app/feature/feature-profile/build.gradle.kts b/app/feature/feature-profile/build.gradle.kts index 5cc2b3e281..9f13300879 100644 --- a/app/feature/feature-profile/build.gradle.kts +++ b/app/feature/feature-profile/build.gradle.kts @@ -41,7 +41,7 @@ dependencies { implementation(projects.coreUiData) implementation(projects.dataSettingsDatastorePublic) implementation(projects.designSystemHedvig) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.languageCore) implementation(projects.languageData) implementation(projects.memberRemindersPublic) diff --git a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/aboutapp/InformationDestination.kt b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/aboutapp/InformationDestination.kt index b7376e25dc..492abcaee5 100644 --- a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/aboutapp/InformationDestination.kt +++ b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/aboutapp/InformationDestination.kt @@ -315,7 +315,8 @@ private fun openEmailClientWithPrefilledData( } private val horizontalDividerModifier = Modifier.horizontalDivider( - DividerPosition.Bottom, horizontalPadding = 16.dp, + DividerPosition.Bottom, + horizontalPadding = 16.dp, ) @Composable @@ -352,6 +353,7 @@ private class LinkContainer( ) { private val privacyPolicyLinkEn = "https://www.hedvig.com/se-en/hedvig/privacy-policy" private val privacyPolicyLinkSe = "https://www.hedvig.com/se/hedvig/personuppgifter" + fun getPrivacyPolicyLink(): String { return when (languageService.getLanguage()) { Language.SV_SE -> privacyPolicyLinkSe @@ -361,6 +363,7 @@ private class LinkContainer( private val legalInfoLinkEn = "https://www.hedvig.com/se-en/hedvig/legal" private val legalInfoLinkSe = "https://www.hedvig.com/se/hedvig/legal" + fun getLegalInfoLink(): String { return when (languageService.getLanguage()) { Language.SV_SE -> legalInfoLinkSe @@ -370,6 +373,7 @@ private class LinkContainer( private val a11yLinkEn = "https://www.hedvig.com/se-en/help/accessibility" private val a11yLinkSe = "https://www.hedvig.com/se/hjalp/tillganglighet" + fun getA11yLink(): String { return when (languageService.getLanguage()) { Language.SV_SE -> a11yLinkSe @@ -378,14 +382,8 @@ private class LinkContainer( } } - @Composable -private fun LinkRow( - link: String, - text: String, - onLinkClick: (String) -> Unit, - modifier: Modifier = Modifier, -) { +private fun LinkRow(link: String, text: String, onLinkClick: (String) -> Unit, modifier: Modifier = Modifier) { Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier @@ -413,7 +411,6 @@ private fun LinkRow( } } - @HedvigPreview @Composable private fun PreviewLinkRow() { @@ -450,12 +447,15 @@ private fun PreviewInformationScreen() { private val previewLanguageService = object : LanguageService { override fun setLanguage(language: Language) {} + override fun getSelectedLanguage(): Language { return Language.EN_SE } + override fun getLanguage(): Language { return Language.EN_SE } + override fun getLocale(): CommonLocale { return CommonLocale.getDefault() } diff --git a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/navigation/ProfileDestinations.kt b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/navigation/ProfileDestinations.kt index 315e3c3f8e..a701649e3c 100644 --- a/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/navigation/ProfileDestinations.kt +++ b/app/feature/feature-profile/src/main/kotlin/com/hedvig/android/feature/profile/navigation/ProfileDestinations.kt @@ -30,7 +30,6 @@ internal sealed interface ProfileDestinations { @Serializable data object SettingsGraph : ProfileDestinations, Destination - } internal sealed interface SettingsDestinations { @@ -46,4 +45,3 @@ val profileBottomNavPermittedDestinations: List> = listO * Not saving navigation state when explicitly logging out from Profile */ val destinationToExcludeFromSavingState: KClass = ProfileDestination.Profile::class - 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 7400c86687..1a3842963a 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 @@ -8,8 +8,8 @@ import com.hedvig.android.core.buildconstants.HedvigBuildConstants import com.hedvig.android.data.coinsured.CoInsuredFlowType import com.hedvig.android.design.system.hedvig.GlobalSnackBarState import com.hedvig.android.design.system.hedvig.motion.MotionDefaults -import com.hedvig.android.feature.profile.aboutapp.InformationDestination import com.hedvig.android.feature.profile.aboutapp.AboutAppViewModel +import com.hedvig.android.feature.profile.aboutapp.InformationDestination import com.hedvig.android.feature.profile.aboutapp.LicensesDestination import com.hedvig.android.feature.profile.certificates.CertificatesDestination import com.hedvig.android.feature.profile.certificates.CertificatesViewModel @@ -17,7 +17,6 @@ import com.hedvig.android.feature.profile.contactinfo.ContactInfoDestination import com.hedvig.android.feature.profile.contactinfo.ContactInfoViewModel import com.hedvig.android.feature.profile.eurobonus.EurobonusDestination import com.hedvig.android.feature.profile.eurobonus.EurobonusViewModel - import com.hedvig.android.feature.profile.navigation.ProfileDestination import com.hedvig.android.feature.profile.navigation.ProfileDestinations import com.hedvig.android.feature.profile.navigation.ProfileDestinations.Certificates diff --git a/app/feature/feature-terminate-insurance/build.gradle.kts b/app/feature/feature-terminate-insurance/build.gradle.kts index 5290ae86ca..9a6ae1c0f5 100644 --- a/app/feature/feature-terminate-insurance/build.gradle.kts +++ b/app/feature/feature-terminate-insurance/build.gradle.kts @@ -20,8 +20,6 @@ dependencies { implementation(libs.androidx.compose.material3.windowSizeClass) implementation(libs.androidx.navigation.compose) implementation(libs.arrow.core) - implementation(libs.compose.richtext) - implementation(libs.compose.richtextCommonmark) implementation(libs.coroutines.core) implementation(libs.jetbrains.lifecycle.runtime.compose) implementation(libs.jetbrains.lifecycle.viewmodel.compose) @@ -38,6 +36,7 @@ dependencies { implementation(projects.dataContract) implementation(projects.dataTermination) implementation(projects.designSystemHedvig) + implementation(projects.featureFlags) implementation(projects.languageCore) implementation(projects.moleculePublic) implementation(projects.navigationCommon) diff --git a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminateInsuranceRepository.kt b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminateInsuranceRepository.kt index 651b811cf9..2e6438832d 100644 --- a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminateInsuranceRepository.kt +++ b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/data/TerminateInsuranceRepository.kt @@ -5,17 +5,17 @@ import arrow.core.left import arrow.core.raise.either import com.apollographql.apollo.ApolloClient import com.apollographql.apollo.api.Optional +import com.apollographql.apollo.cache.normalized.FetchPolicy +import com.apollographql.apollo.cache.normalized.fetchPolicy import com.hedvig.android.apollo.ErrorMessage import com.hedvig.android.apollo.safeExecute import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.logger.logcat import kotlinx.datetime.LocalDate import octopus.DeleteContractMutation import octopus.TerminateContractMutation import octopus.TerminationSurveyQuery import octopus.fragment.TerminationSurveyOptionSuggestionFragment -import com.apollographql.apollo.cache.normalized.FetchPolicy -import com.apollographql.apollo.cache.normalized.fetchPolicy -import com.hedvig.android.logger.logcat import octopus.type.TerminationFlowDeleteContractInput import octopus.type.TerminationFlowSurveyOptionSuggestionType import octopus.type.TerminationFlowTerminateContractInput diff --git a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyDestination.kt b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyDestination.kt index b00b65f55a..342c955cdd 100644 --- a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyDestination.kt +++ b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/step/survey/TerminationSurveyDestination.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed -import com.halilibo.richtext.commonmark.Markdown import com.hedvig.android.data.changetier.data.IntentOutput import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize.Large import com.hedvig.android.design.system.hedvig.EmptyState @@ -35,6 +34,7 @@ import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateIcon import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateIconStyle.INFO import com.hedvig.android.design.system.hedvig.HedvigButton import com.hedvig.android.design.system.hedvig.HedvigDialog +import com.hedvig.android.design.system.hedvig.HedvigMarkdownText import com.hedvig.android.design.system.hedvig.HedvigNotificationCard import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigTextButton @@ -45,7 +45,6 @@ import com.hedvig.android.design.system.hedvig.ProvideTextStyle import com.hedvig.android.design.system.hedvig.RadioGroup import com.hedvig.android.design.system.hedvig.RadioOption import com.hedvig.android.design.system.hedvig.RadioOptionId -import com.hedvig.android.design.system.hedvig.RichText import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.design.system.hedvig.a11y.FlowHeading import com.hedvig.android.design.system.hedvig.freetext.FreeTextDisplay @@ -300,11 +299,7 @@ private fun SelectedSurveyInfoBox( ProvideTextStyle( HedvigTheme.typography.label, ) { - RichText { - Markdown( - content = suggestion.description, - ) - } + HedvigMarkdownText(content = suggestion.description) } }, priority = when (suggestion.type) { diff --git a/app/feature/feature-travel-certificate/build.gradle.kts b/app/feature/feature-travel-certificate/build.gradle.kts index 24cc51e199..baa03ba70b 100644 --- a/app/feature/feature-travel-certificate/build.gradle.kts +++ b/app/feature/feature-travel-certificate/build.gradle.kts @@ -30,7 +30,7 @@ dependencies { implementation(projects.dataAddons) implementation(projects.dataContract) implementation(projects.designSystemHedvig) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.languageCore) implementation(projects.moleculePublic) implementation(projects.navigationActivity) diff --git a/app/featureflags/feature-flags-android/build.gradle.kts b/app/featureflags/feature-flags-android/build.gradle.kts deleted file mode 100644 index 9e4f325fa1..0000000000 --- a/app/featureflags/feature-flags-android/build.gradle.kts +++ /dev/null @@ -1,16 +0,0 @@ -plugins { - id("hedvig.android.library") - id("hedvig.gradle.plugin") -} - -dependencies { - api(libs.unleash) - - implementation(libs.coroutines.core) - implementation(libs.koin.core) - implementation(projects.authCorePublic) - implementation(projects.authEventCore) - implementation(projects.coreBuildConstants) - implementation(projects.coreCommonPublic) - implementation(projects.featureFlagsPublic) -} diff --git a/app/featureflags/feature-flags-public/build.gradle.kts b/app/featureflags/feature-flags-public/build.gradle.kts deleted file mode 100644 index a1bc0a3d59..0000000000 --- a/app/featureflags/feature-flags-public/build.gradle.kts +++ /dev/null @@ -1,13 +0,0 @@ -plugins { - id("hedvig.jvm.library") - id("hedvig.gradle.plugin") -} - -dependencies { - implementation(libs.coroutines.core) - implementation(libs.koin.core) - implementation(projects.authCorePublic) - implementation(projects.authEventCore) - implementation(projects.coreBuildConstants) - implementation(projects.coreCommonPublic) -} diff --git a/app/featureflags/feature-flags-test/build.gradle.kts b/app/featureflags/feature-flags-test/build.gradle.kts index 5f2a6527a9..65adb4ad55 100644 --- a/app/featureflags/feature-flags-test/build.gradle.kts +++ b/app/featureflags/feature-flags-test/build.gradle.kts @@ -5,5 +5,5 @@ plugins { dependencies { implementation(libs.turbine) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) } diff --git a/app/featureflags/feature-flags/build.gradle.kts b/app/featureflags/feature-flags/build.gradle.kts new file mode 100644 index 0000000000..d8716f00a4 --- /dev/null +++ b/app/featureflags/feature-flags/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + id("hedvig.multiplatform.library") + id("hedvig.multiplatform.library.android") + id("hedvig.gradle.plugin") + alias(libs.plugins.kmpNativeCoroutines) +} + +hedvig { + serialization() +} + +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.coroutines.core) + implementation(libs.koin.core) + } + androidMain.dependencies { + implementation(libs.unleash) + implementation(projects.authCorePublic) + implementation(projects.authEventCore) + implementation(projects.coreBuildConstants) + implementation(projects.coreCommonPublic) + } + } +} diff --git a/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/HedvigUnleashClient.kt b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/HedvigUnleashClient.kt similarity index 100% rename from app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/HedvigUnleashClient.kt rename to app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/HedvigUnleashClient.kt diff --git a/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt similarity index 95% rename from app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt rename to app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt index 0f7a46181b..fca7781f02 100644 --- a/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt +++ b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt @@ -9,7 +9,7 @@ import com.hedvig.android.featureflags.HedvigUnleashClient import com.hedvig.android.featureflags.flags.UnleashFeatureFlagProvider import org.koin.dsl.module -val featureManagerModule = module { +actual val featureManagerModule = module { single { HedvigUnleashClient( androidContext = get().applicationContext, diff --git a/app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt similarity index 100% rename from app/featureflags/feature-flags-android/src/main/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt rename to app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/UnleashFeatureFlagProvider.kt diff --git a/app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/FeatureManager.kt b/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/FeatureManager.kt similarity index 72% rename from app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/FeatureManager.kt rename to app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/FeatureManager.kt index 8e1041bd72..c475b01ce4 100644 --- a/app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/FeatureManager.kt +++ b/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/FeatureManager.kt @@ -1,8 +1,10 @@ package com.hedvig.android.featureflags import com.hedvig.android.featureflags.flags.Feature +import com.rickclephas.kmp.nativecoroutines.NativeCoroutines import kotlinx.coroutines.flow.Flow interface FeatureManager { + @NativeCoroutines fun isFeatureEnabled(feature: Feature): Flow } diff --git a/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt b/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt new file mode 100644 index 0000000000..34de889fe4 --- /dev/null +++ b/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt @@ -0,0 +1,5 @@ +package com.hedvig.android.featureflags.di + +import org.koin.core.module.Module + +expect val featureManagerModule: Module diff --git a/app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/flags/Feature.kt b/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt similarity index 100% rename from app/featureflags/feature-flags-public/src/main/kotlin/com/hedvig/android/featureflags/flags/Feature.kt rename to app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt diff --git a/app/featureflags/feature-flags/src/iosMain/kotlin/com/hedvig/android/featureflags/di/IosFeatureManager.kt b/app/featureflags/feature-flags/src/iosMain/kotlin/com/hedvig/android/featureflags/di/IosFeatureManager.kt new file mode 100644 index 0000000000..1e83c24fb2 --- /dev/null +++ b/app/featureflags/feature-flags/src/iosMain/kotlin/com/hedvig/android/featureflags/di/IosFeatureManager.kt @@ -0,0 +1,14 @@ +package com.hedvig.android.featureflags.di + +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.Feature +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withContext + +class IosFeatureManager(private val isFeatureEnabledBlock: (Feature) -> Boolean) : FeatureManager { + override fun isFeatureEnabled(feature: Feature): Flow = flow { + emit(withContext(Dispatchers.Main.immediate) { isFeatureEnabledBlock(feature) }) + } +} diff --git a/app/featureflags/feature-flags/src/iosMain/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt b/app/featureflags/feature-flags/src/iosMain/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt new file mode 100644 index 0000000000..612bbc65a9 --- /dev/null +++ b/app/featureflags/feature-flags/src/iosMain/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt @@ -0,0 +1,5 @@ +package com.hedvig.android.featureflags.di + +import org.koin.dsl.module + +actual val featureManagerModule = module {} diff --git a/app/featureflags/feature-flags/src/jvmMain/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt b/app/featureflags/feature-flags/src/jvmMain/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt new file mode 100644 index 0000000000..d075054446 --- /dev/null +++ b/app/featureflags/feature-flags/src/jvmMain/kotlin/com/hedvig/android/featureflags/di/featureManagerModule.kt @@ -0,0 +1,11 @@ +package com.hedvig.android.featureflags.di + +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.NoopFeatureFlagProvider +import org.koin.dsl.module + +actual val featureManagerModule = module { + single { + NoopFeatureFlagProvider() + } +} diff --git a/app/featureflags/feature-flags/src/jvmMain/kotlin/com/hedvig/android/featureflags/flags/NoopFeatureFlagProvider.kt b/app/featureflags/feature-flags/src/jvmMain/kotlin/com/hedvig/android/featureflags/flags/NoopFeatureFlagProvider.kt new file mode 100644 index 0000000000..3033fdafd7 --- /dev/null +++ b/app/featureflags/feature-flags/src/jvmMain/kotlin/com/hedvig/android/featureflags/flags/NoopFeatureFlagProvider.kt @@ -0,0 +1,11 @@ +package com.hedvig.android.featureflags.flags + +import com.hedvig.android.featureflags.FeatureManager +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +internal class NoopFeatureFlagProvider : FeatureManager { + override fun isFeatureEnabled(feature: Feature): Flow { + return flowOf(false) + } +} diff --git a/app/language/language-core/src/nativeMain/kotlin/com/hedvig/android/language/LanguageService.native.kt b/app/language/language-core/src/nativeMain/kotlin/com/hedvig/android/language/LanguageService.native.kt index 52c839a00b..a795319f6e 100644 --- a/app/language/language-core/src/nativeMain/kotlin/com/hedvig/android/language/LanguageService.native.kt +++ b/app/language/language-core/src/nativeMain/kotlin/com/hedvig/android/language/LanguageService.native.kt @@ -1,22 +1,25 @@ package com.hedvig.android.language import com.hedvig.android.core.locale.CommonLocale +import platform.Foundation.NSLocale -// todo ios -internal class NativeLanguageService : LanguageService { +internal class NativeLanguageService( + private val storage: LanguageStorage, +) : LanguageService { override fun setLanguage(language: Language) { - TODO("Not yet implemented") + storage.setLanguageTag(language.toBcp47Format()) } override fun getSelectedLanguage(): Language? { - TODO("Not yet implemented") + return storage.getSelectedLanguageTag()?.let(Language::from) } override fun getLanguage(): Language { - TODO("Not yet implemented") + val languageTag = storage.getCurrentLanguageTag() + return Language.from(languageTag) } override fun getLocale(): CommonLocale { - TODO("Not yet implemented") + return NSLocale(localeIdentifier = storage.getCurrentLanguageTag()) } } diff --git a/app/language/language-core/src/nativeMain/kotlin/com/hedvig/android/language/LanguageStorage.kt b/app/language/language-core/src/nativeMain/kotlin/com/hedvig/android/language/LanguageStorage.kt new file mode 100644 index 0000000000..6525a256af --- /dev/null +++ b/app/language/language-core/src/nativeMain/kotlin/com/hedvig/android/language/LanguageStorage.kt @@ -0,0 +1,13 @@ +package com.hedvig.android.language + +/** + * iOS-side bridge that provides locale state to the shared Kotlin layer. + * The Swift implementation must perform mutations on the main thread. + */ +interface LanguageStorage { + fun getCurrentLanguageTag(): String + + fun getSelectedLanguageTag(): String? + + fun setLanguageTag(tag: String) +} diff --git a/app/language/language-core/src/nativeMain/kotlin/com/hedvig/android/language/di/languageModule.native.kt b/app/language/language-core/src/nativeMain/kotlin/com/hedvig/android/language/di/languageModule.native.kt index 3c6a82748a..af9fa320e7 100644 --- a/app/language/language-core/src/nativeMain/kotlin/com/hedvig/android/language/di/languageModule.native.kt +++ b/app/language/language-core/src/nativeMain/kotlin/com/hedvig/android/language/di/languageModule.native.kt @@ -6,5 +6,5 @@ import org.koin.core.module.Module import org.koin.dsl.module internal actual val platformLanguageModule: Module = module { - single { NativeLanguageService() } + single { NativeLanguageService(get()) } } diff --git a/app/logging/logging-public/src/nativeMain/kotlin/com/hedvig/android/logger/IosLogcatLogger.kt b/app/logging/logging-public/src/nativeMain/kotlin/com/hedvig/android/logger/IosLogcatLogger.kt new file mode 100644 index 0000000000..279252ac08 --- /dev/null +++ b/app/logging/logging-public/src/nativeMain/kotlin/com/hedvig/android/logger/IosLogcatLogger.kt @@ -0,0 +1,28 @@ +package com.hedvig.android.logger + +import platform.Foundation.NSLog + +class IosLogcatLogger : LogcatLogger { + override fun log(priority: LogPriority, throwable: Throwable?, tag: String?, message: () -> String) { + val prefix = when (priority) { + LogPriority.VERBOSE -> "V" + LogPriority.DEBUG -> "D" + LogPriority.INFO -> "I" + LogPriority.WARN -> "W" + LogPriority.ERROR -> "E" + LogPriority.ASSERT -> "A" + } + val tagPart = if (tag != null) "[$tag] " else "" + val messagePart = message() + val throwablePart = if (throwable != null) "\n${throwable.stackTraceToString()}" else "" + NSLog("%s", "$prefix/$tagPart$messagePart$throwablePart") + } + + companion object { + fun install() { + if (!LogcatLogger.isInstalled) { + LogcatLogger.install(IosLogcatLogger()) + } + } + } +} diff --git a/app/member-reminders/member-reminders-public/build.gradle.kts b/app/member-reminders/member-reminders-public/build.gradle.kts index 7ff1c4d7cf..e1eebfbf95 100644 --- a/app/member-reminders/member-reminders-public/build.gradle.kts +++ b/app/member-reminders/member-reminders-public/build.gradle.kts @@ -28,7 +28,7 @@ dependencies { implementation(projects.coreDemoMode) implementation(projects.dataContract) implementation(projects.dataPayingMember) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) testImplementation(libs.apollo.annotations) testImplementation(libs.apollo.testingSupport) diff --git a/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCaseTest.kt b/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCaseTest.kt index 59401d599f..3dc189f9f0 100644 --- a/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCaseTest.kt +++ b/app/member-reminders/member-reminders-public/src/test/kotlin/com/hedvig/android/memberreminders/GetMemberRemindersUseCaseTest.kt @@ -41,7 +41,7 @@ class GetMemberRemindersUseCaseTest { getUpcomingRenewalRemindersUseCase = getUpcomingRenewalRemindersUseCase, getNeedsCoInsuredInfoRemindersUseCase = getNeedsCoInsuredInfoRemindersUseCase, getContactInfoUpdateIsNeededUseCase = getContactInfoUpdateIsNeededUseCase, - getMissingChipIdReminderUseCase = getMissingChipIdReminderUseCase + getMissingChipIdReminderUseCase = getMissingChipIdReminderUseCase, ) getMemberRemindersUseCase.invoke().test { @@ -75,7 +75,7 @@ class GetMemberRemindersUseCaseTest { getUpcomingRenewalRemindersUseCase = getUpcomingRenewalRemindersUseCase, getNeedsCoInsuredInfoRemindersUseCase = getNeedsCoInsuredInfoRemindersUseCase, getContactInfoUpdateIsNeededUseCase = getContactInfoUpdateIsNeededUseCase, - getMissingChipIdReminderUseCase = getMissingChipIdReminderUseCase + getMissingChipIdReminderUseCase = getMissingChipIdReminderUseCase, ) val testId = "test" @@ -147,5 +147,4 @@ class GetMemberRemindersUseCaseTest { ) } } - } 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 c6fea8b964..8499d2b422 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 @@ -66,47 +66,50 @@ import org.jetbrains.compose.resources.stringResource @Composable fun getMemberReminderMessage(reminder: MemberReminder): String { return when (reminder) { - is MemberReminder.CoInsuredInfo -> stringResource( - when (reminder.coInsuredType) { - CoInsuredFlowType.CoInsured -> Res.string.CONTRACT_COINSURED_MISSING_INFO_TEXT - CoInsuredFlowType.CoOwners -> Res.string.CONTRACT_COOWNERS_MISSING_INFO_TEXT - } - ) + is MemberReminder.CoInsuredInfo -> { + stringResource( + when (reminder.coInsuredType) { + CoInsuredFlowType.CoInsured -> Res.string.CONTRACT_COINSURED_MISSING_INFO_TEXT + CoInsuredFlowType.CoOwners -> Res.string.CONTRACT_COOWNERS_MISSING_INFO_TEXT + }, + ) + } - is MemberReminder.PaymentReminder.ConnectPayment -> + is MemberReminder.PaymentReminder.ConnectPayment -> { stringResource(Res.string.info_card_missing_payment_body) + } - is MemberReminder.PaymentReminder.ConnectPayout -> + is MemberReminder.PaymentReminder.ConnectPayout -> { stringResource(Res.string.PAYOUT_MISSING_INFO) + } - is MemberReminder.PaymentReminder.TerminationDueToMissedPayments -> + is MemberReminder.PaymentReminder.TerminationDueToMissedPayments -> { stringResource(Res.string.info_card_missing_payment_missing_payments_body, reminder.terminationDate) + } - is UpcomingRenewal -> - { + is UpcomingRenewal -> { val daysUntilRenewal = remember(TimeZone.currentSystemDefault(), reminder.renewalDate) { daysUntil(reminder.renewalDate) } stringResource(Res.string.DASHBOARD_RENEWAL_PROMPTER_BODY, daysUntilRenewal) } - - is MemberReminder.EnableNotifications -> + is MemberReminder.EnableNotifications -> { stringResource(Res.string.PROFILE_ALLOW_NOTIFICATIONS_INFO_LABEL) + } - is MemberReminder.ContactInfoUpdateNeeded -> + is MemberReminder.ContactInfoUpdateNeeded -> { stringResource(Res.string.MISSING_CONTACT_INFO_CARD_TEXT) + } - is MemberReminder.MissingChipId -> + is MemberReminder.MissingChipId -> { stringResource(Res.string.CHIP_ID_MISSING_MESSAGE) + } } } @Composable -fun rememberMaxLineCountForReminders( - memberReminders: List, - maxWidthPx: Int, -): Int { +fun rememberMaxLineCountForReminders(memberReminders: List, maxWidthPx: Int): Int { val textMeasurer = rememberTextMeasurer() val density = LocalDensity.current val fontFamilyResolver = LocalFontFamilyResolver.current @@ -186,7 +189,7 @@ fun MemberReminderCards( navigateToContactInfo = navigateToContactInfo, navigateToChipId = navigateToChipId, modifier = modifier.padding(contentPadding), - minLines = 1 + minLines = 1, ) } else if (memberReminders.isNotEmpty()) { val stableReminderIds = remember(memberReminders.map { it.id }) { @@ -204,7 +207,7 @@ fun MemberReminderCards( BoxWithConstraints(Modifier.fillMaxWidth()) { val minLineCount = rememberMaxLineCountForReminders( memberReminders = memberReminders, - maxWidthPx = constraints.maxWidth + maxWidthPx = constraints.maxWidth, ) Column { HorizontalPager( @@ -223,15 +226,16 @@ fun MemberReminderCards( navigateToAddMissingInfo = navigateToAddMissingInfo, navigateToConnectPayment = navigateToConnectPayment, navigateToConnectPayout = navigateToConnectPayout, - openUrl = openUrl, - onNavigateToNewConversation = onNavigateToNewConversation, - snoozeNotificationPermissionReminder = snoozeNotificationPermissionReminder, - notificationPermissionState = notificationPermissionState, - navigateToContactInfo = navigateToContactInfo, - navigateToChipId = navigateToChipId, - modifier = modifier.fillMaxWidth(), - minLines = minLineCount - )} + openUrl = openUrl, + onNavigateToNewConversation = onNavigateToNewConversation, + snoozeNotificationPermissionReminder = snoozeNotificationPermissionReminder, + notificationPermissionState = notificationPermissionState, + navigateToContactInfo = navigateToContactInfo, + navigateToChipId = navigateToChipId, + modifier = modifier.fillMaxWidth(), + minLines = minLineCount, + ) + } } } } @@ -400,11 +404,7 @@ fun ReminderCardUpdateContactInfo( } @Composable -internal fun ReminderMissingChipId( - navigateToChipId: () -> Unit, - minLines: Int, - modifier: Modifier = Modifier, -) { +internal fun ReminderMissingChipId(navigateToChipId: () -> Unit, minLines: Int, modifier: Modifier = Modifier) { val message = getMemberReminderMessage(MemberReminder.MissingChipId()) HedvigNotificationCard( message = message, @@ -414,7 +414,7 @@ internal fun ReminderMissingChipId( buttonText = stringResource(Res.string.CHIP_ID_MISSING_BUTTON), onButtonClick = navigateToChipId, ), - minLines = minLines + minLines = minLines, ) } @@ -541,7 +541,7 @@ private fun PreviewReminderCardConnectPayment() { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { ReminderCardConnectPayment( navigateToConnectPayment = {}, - memberReminder = MemberReminder.PaymentReminder.ConnectPayment() + memberReminder = MemberReminder.PaymentReminder.ConnectPayment(), ) } } @@ -555,7 +555,8 @@ private fun PreviewReminderCardMissingPayment() { ReminderCardConnectPayment( navigateToConnectPayment = {}, memberReminder = MemberReminder.PaymentReminder.TerminationDueToMissedPayments( - terminationDate = LocalDate(2029,1,1)) + terminationDate = LocalDate(2029, 1, 1), + ), ) } } @@ -569,7 +570,7 @@ private fun PreviewReminderCardUpcomingRenewals() { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { ReminderCardUpcomingRenewals( openUrl = {}, - memberReminder = upcomingRenewal + memberReminder = upcomingRenewal, ) } } @@ -607,7 +608,7 @@ private fun PreviewReminderMissingChipId() { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { ReminderMissingChipId( navigateToChipId = {}, - minLines = 1 + minLines = 1, ) } } diff --git a/app/molecule/molecule-public/build.gradle.kts b/app/molecule/molecule-public/build.gradle.kts index 09f54b8a13..1f27ffc6b3 100644 --- a/app/molecule/molecule-public/build.gradle.kts +++ b/app/molecule/molecule-public/build.gradle.kts @@ -12,8 +12,8 @@ kotlin { sourceSets { commonMain.dependencies { api(libs.jetbrains.lifecycle.viewmodel) + api(libs.molecule) implementation(libs.coroutines.core) - implementation(libs.molecule) implementation(libs.jetbrains.compose.runtime) implementation(libs.coroutines.core) } diff --git a/app/molecule/molecule-public/src/nativeMain/kotlin/com/hedvig/android/molecule/public/MoleculeViewModel.native.kt b/app/molecule/molecule-public/src/nativeMain/kotlin/com/hedvig/android/molecule/public/MoleculeViewModel.native.kt index 6cd0164580..730d73638c 100644 --- a/app/molecule/molecule-public/src/nativeMain/kotlin/com/hedvig/android/molecule/public/MoleculeViewModel.native.kt +++ b/app/molecule/molecule-public/src/nativeMain/kotlin/com/hedvig/android/molecule/public/MoleculeViewModel.native.kt @@ -2,5 +2,6 @@ package com.hedvig.android.molecule.public import app.cash.molecule.DisplayLinkClock import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.Dispatchers -internal actual val mainDispatcher: CoroutineContext = DisplayLinkClock +internal actual val mainDispatcher: CoroutineContext = Dispatchers.Main + DisplayLinkClock diff --git a/app/navigation/navigation-compose-typed/build.gradle.kts b/app/navigation/navigation-compose-typed/build.gradle.kts index 6556e43c00..c5caff6eb5 100644 --- a/app/navigation/navigation-compose-typed/build.gradle.kts +++ b/app/navigation/navigation-compose-typed/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") } @@ -8,12 +9,16 @@ hedvig { compose() } -dependencies { - implementation(libs.androidx.navigation.common) - implementation(libs.androidx.navigation.runtime) - implementation(libs.jetbrains.compose.runtime) - implementation(libs.koin.composeViewModel) - implementation(libs.kotlinx.serialization.core) - implementation(projects.navigationCommon) - implementation(projects.navigationCompose) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.jetbrains.navigation.compose) + implementation(libs.jetbrains.compose.runtime) + implementation(libs.jetbrains.lifecycle.viewmodel) + implementation(libs.koin.composeViewModel) + implementation(libs.kotlinx.serialization.core) + implementation(projects.navigationCommon) + implementation(projects.navigationCompose) + } + } } diff --git a/app/navigation/navigation-compose-typed/src/main/kotlin/com/hedvig/android/navigation/compose/typed/DestinationScopedViewModel.kt b/app/navigation/navigation-compose-typed/src/commonMain/kotlin/com/hedvig/android/navigation/compose/typed/DestinationScopedViewModel.kt similarity index 100% rename from app/navigation/navigation-compose-typed/src/main/kotlin/com/hedvig/android/navigation/compose/typed/DestinationScopedViewModel.kt rename to app/navigation/navigation-compose-typed/src/commonMain/kotlin/com/hedvig/android/navigation/compose/typed/DestinationScopedViewModel.kt diff --git a/app/navigation/navigation-compose-typed/src/main/kotlin/com/hedvig/android/navigation/compose/typed/getRouteFromBackStack.kt b/app/navigation/navigation-compose-typed/src/commonMain/kotlin/com/hedvig/android/navigation/compose/typed/getRouteFromBackStack.kt similarity index 100% rename from app/navigation/navigation-compose-typed/src/main/kotlin/com/hedvig/android/navigation/compose/typed/getRouteFromBackStack.kt rename to app/navigation/navigation-compose-typed/src/commonMain/kotlin/com/hedvig/android/navigation/compose/typed/getRouteFromBackStack.kt diff --git a/app/navigation/navigation-compose-typed/src/main/AndroidManifest.xml b/app/navigation/navigation-compose-typed/src/main/AndroidManifest.xml deleted file mode 100644 index 568741e54f..0000000000 --- a/app/navigation/navigation-compose-typed/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file 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 d8aa749936..359934f05e 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 @@ -7,6 +7,7 @@ interface HedvigDeepLinkContainer { val helpCenter: List // The help center root screen val helpCenterCommonTopic: List // A common topic inside the help center val helpCenterQuestion: List // A specific question inside the help center + val puppyGuide: List // The puppy guide list screen, inside the help center val insurances: List // The insurances destination, which also shows cross sells val claimFlow: List // The claim flow starting pledge destination @@ -105,6 +106,10 @@ internal class HedvigDeepLinkContainerImpl( "$baseDeepLinkDomain/help-center/question?id={id}" } + override val puppyGuide: List = baseDeepLinkDomains.map { baseDeepLinkDomain -> + "$baseDeepLinkDomain/puppy-guide" + } + override val insurances: List = baseDeepLinkDomains.map { baseDeepLinkDomain -> "$baseDeepLinkDomain/insurances" } @@ -241,6 +246,7 @@ val HedvigDeepLinkContainer.allDeepLinkUriPatterns: List petIdWithContractId.first(), petIdWithoutContractId.first(), profile.first(), + puppyGuide.first(), terminateInsurance.first(), travelAddon.first(), travelAddonWithContractId.first(), diff --git a/app/network/network-clients/src/commonMain/kotlin/com/hedvig/android/network/clients/di/NetworkModule.kt b/app/network/network-clients/src/commonMain/kotlin/com/hedvig/android/network/clients/di/NetworkModule.kt index 85ea20df6b..d8310ba2e8 100644 --- a/app/network/network-clients/src/commonMain/kotlin/com/hedvig/android/network/clients/di/NetworkModule.kt +++ b/app/network/network-clients/src/commonMain/kotlin/com/hedvig/android/network/clients/di/NetworkModule.kt @@ -96,7 +96,10 @@ private fun DefaultRequest.DefaultRequestBuilder.commonHeaders( append("X-System-Version", hedvigBuildConstants.buildApiVersion.toString()) append("X-Platform", hedvigBuildConstants.platformName) append("X-Model", hedvigBuildConstants.model) - append("Hedvig-App-Version", "android;${hedvigBuildConstants.appVersionName}") + append( + "Hedvig-App-Version", + "${hedvigBuildConstants.platformName.lowercase()};${hedvigBuildConstants.appVersionName}", + ) } } diff --git a/app/network/network-clients/src/nativeMain/kotlin/com/hedvig/android/network/clients/IosAuthTokenInterceptor.kt b/app/network/network-clients/src/nativeMain/kotlin/com/hedvig/android/network/clients/IosAuthTokenInterceptor.kt deleted file mode 100644 index a1ee643b19..0000000000 --- a/app/network/network-clients/src/nativeMain/kotlin/com/hedvig/android/network/clients/IosAuthTokenInterceptor.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.hedvig.android.network.clients - -import com.apollographql.apollo.api.ApolloRequest -import com.apollographql.apollo.api.ApolloResponse -import com.apollographql.apollo.api.Operation -import com.apollographql.apollo.interceptor.ApolloInterceptor -import com.apollographql.apollo.interceptor.ApolloInterceptorChain -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow - -internal class IosAuthTokenInterceptor( - private val accessTokenFetcher: AccessTokenFetcher, -) : ApolloInterceptor { - override fun intercept( - request: ApolloRequest, - chain: ApolloInterceptorChain, - ): Flow> { - return flow { - emitAll( - chain.proceed( - request - .newBuilder() - .run { - if (accessTokenFetcher.fetch() != null) { - addHttpHeader("Authorization", "Bearer ${accessTokenFetcher.fetch()}") - } else { - this - } - } - .build(), - ), - ) - } - } -} diff --git a/app/network/network-clients/src/nativeMain/kotlin/com/hedvig/android/network/clients/IosExtraApolloClientConfiguration.kt b/app/network/network-clients/src/nativeMain/kotlin/com/hedvig/android/network/clients/IosExtraApolloClientConfiguration.kt deleted file mode 100644 index 3ff6344d49..0000000000 --- a/app/network/network-clients/src/nativeMain/kotlin/com/hedvig/android/network/clients/IosExtraApolloClientConfiguration.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.hedvig.android.network.clients - -import com.apollographql.apollo.ApolloClient -import com.apollographql.apollo.network.http.DefaultHttpEngine - -internal class IosExtraApolloClientConfiguration( - private val iosAuthTokenInterceptor: IosAuthTokenInterceptor, -) : ExtraApolloClientConfiguration { - override fun configure(builder: ApolloClient.Builder): ApolloClient.Builder { - return builder - .addInterceptor(iosAuthTokenInterceptor) - .httpEngine(DefaultHttpEngine()) - } -} diff --git a/app/network/network-clients/src/nativeMain/kotlin/com/hedvig/android/network/clients/di/NetworkModule.native.kt b/app/network/network-clients/src/nativeMain/kotlin/com/hedvig/android/network/clients/di/NetworkModule.native.kt index 6bf44e0905..20ff888e97 100644 --- a/app/network/network-clients/src/nativeMain/kotlin/com/hedvig/android/network/clients/di/NetworkModule.native.kt +++ b/app/network/network-clients/src/nativeMain/kotlin/com/hedvig/android/network/clients/di/NetworkModule.native.kt @@ -1,17 +1,12 @@ package com.hedvig.android.network.clients.di -import com.hedvig.android.network.clients.AccessTokenFetcher import com.hedvig.android.network.clients.ExtraApolloClientConfiguration -import com.hedvig.android.network.clients.IosAuthTokenInterceptor -import com.hedvig.android.network.clients.IosExtraApolloClientConfiguration +import com.hedvig.android.network.clients.NoopExtraApolloClientConfiguration import org.koin.core.module.Module import org.koin.dsl.module internal actual val platformNetworkModule: Module = module { single { - IosExtraApolloClientConfiguration(get()) - } - single { - IosAuthTokenInterceptor(get()) + NoopExtraApolloClientConfiguration() } } diff --git a/app/notification-badge-data/notification-badge-data-public/build.gradle.kts b/app/notification-badge-data/notification-badge-data-public/build.gradle.kts index 4fc395693e..6d35b9e6e4 100644 --- a/app/notification-badge-data/notification-badge-data-public/build.gradle.kts +++ b/app/notification-badge-data/notification-badge-data-public/build.gradle.kts @@ -16,7 +16,7 @@ dependencies { implementation(projects.apolloCore) implementation(projects.apolloOctopusPublic) implementation(projects.coreDemoMode) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.languageCore) testImplementation(libs.assertK) diff --git a/app/notification/notification-firebase/build.gradle.kts b/app/notification/notification-firebase/build.gradle.kts index 8d5e4aa026..91349d8fd9 100644 --- a/app/notification/notification-firebase/build.gradle.kts +++ b/app/notification/notification-firebase/build.gradle.kts @@ -22,6 +22,6 @@ dependencies { implementation(projects.authEventCore) implementation(projects.coreCommonPublic) implementation(projects.coreDemoMode) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.notificationCore) } diff --git a/app/shared/partners-deflect/src/commonMain/kotlin/com/hedvig/android/shared/partners/deflect/Deflect.kt b/app/shared/partners-deflect/src/commonMain/kotlin/com/hedvig/android/shared/partners/deflect/Deflect.kt index 7ca3e23ca7..d3b96ecd47 100644 --- a/app/shared/partners-deflect/src/commonMain/kotlin/com/hedvig/android/shared/partners/deflect/Deflect.kt +++ b/app/shared/partners-deflect/src/commonMain/kotlin/com/hedvig/android/shared/partners/deflect/Deflect.kt @@ -13,7 +13,6 @@ data class DeflectData( val faq: List, val buttonText: String, ) { - @Serializable sealed interface DeflectPartnerContainer { @Serializable diff --git a/app/shared/partners-deflect/src/commonMain/kotlin/com/hedvig/android/shared/partners/deflect/ui/PartnerDeflectDestination.kt b/app/shared/partners-deflect/src/commonMain/kotlin/com/hedvig/android/shared/partners/deflect/ui/PartnerDeflectDestination.kt index 211bd50382..1ff52205ee 100644 --- a/app/shared/partners-deflect/src/commonMain/kotlin/com/hedvig/android/shared/partners/deflect/ui/PartnerDeflectDestination.kt +++ b/app/shared/partners-deflect/src/commonMain/kotlin/com/hedvig/android/shared/partners/deflect/ui/PartnerDeflectDestination.kt @@ -38,6 +38,7 @@ import com.hedvig.android.design.system.hedvig.ButtonDefaults import com.hedvig.android.design.system.hedvig.HedvigButton import com.hedvig.android.design.system.hedvig.HedvigErrorSection import com.hedvig.android.design.system.hedvig.HedvigNotificationCard +import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigText import com.hedvig.android.design.system.hedvig.HedvigTheme import com.hedvig.android.design.system.hedvig.HorizontalDivider @@ -57,7 +58,6 @@ import hedvig.resources.SUBMIT_CLAIM_NEED_HELP_LABEL import hedvig.resources.SUBMIT_CLAIM_NEED_HELP_TITLE import hedvig.resources.general_back_button import org.jetbrains.compose.resources.stringResource -import com.hedvig.android.design.system.hedvig.HedvigPreview @Composable fun PartnerDeflectDestination( diff --git a/app/shared/tier-comparison/build.gradle.kts b/app/shared/tier-comparison/build.gradle.kts index dd7c60bcfb..4e20430f5e 100644 --- a/app/shared/tier-comparison/build.gradle.kts +++ b/app/shared/tier-comparison/build.gradle.kts @@ -27,7 +27,7 @@ dependencies { implementation(projects.coreCommonPublic) implementation(projects.coreResources) implementation(projects.designSystemHedvig) - implementation(projects.featureFlagsPublic) + implementation(projects.featureFlags) implementation(projects.languageCore) implementation(projects.moleculePublic) } diff --git a/app/shareddi/build.gradle.kts b/app/shareddi/build.gradle.kts index a34d17f156..de8112e567 100644 --- a/app/shareddi/build.gradle.kts +++ b/app/shareddi/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi - plugins { id("hedvig.multiplatform.library") id("hedvig.multiplatform.library.android") @@ -13,6 +11,8 @@ kotlin { implementation(libs.apollo.engine.ktor) implementation(libs.apollo.normalizedCache) implementation(libs.apollo.runtime) + implementation(libs.coil.coil) + implementation(libs.coil.network.ktor) implementation(libs.koin.core) implementation(libs.ktor.client.auth) implementation(libs.ktor.client.logging) @@ -21,6 +21,8 @@ kotlin { implementation(projects.coreCommonPublic) implementation(projects.coreDatastorePublic) implementation(projects.featureClaimChat) + implementation(projects.featureFlags) + implementation(projects.featureHelpCenter) implementation(projects.languageCore) implementation(projects.networkClients) implementation(projects.permissionCore) diff --git a/app/shareddi/src/commonMain/kotlin/com/hedvig/android/shareddi/SharedModule.kt b/app/shareddi/src/commonMain/kotlin/com/hedvig/android/shareddi/SharedModule.kt index 9cdde84ded..574d17494d 100644 --- a/app/shareddi/src/commonMain/kotlin/com/hedvig/android/shareddi/SharedModule.kt +++ b/app/shareddi/src/commonMain/kotlin/com/hedvig/android/shareddi/SharedModule.kt @@ -1,15 +1,18 @@ package com.hedvig.android.shareddi -import com.apollographql.apollo.ApolloClient import com.hedvig.android.core.buildconstants.AppBuildConfig -import com.hedvig.android.core.datastore.di.dataStoreModule +import com.hedvig.android.core.buildconstants.di.buildConstantsModule import com.hedvig.android.network.clients.di.networkModule import org.koin.core.module.Module import org.koin.dsl.module fun sharedModule(appBuildConfig: AppBuildConfig) = module { single { appBuildConfig } - includes(platformModule, networkModule, dataStoreModule) + includes( + buildConstantsModule, + networkModule, + platformModule, + ) } internal expect val platformModule: Module diff --git a/app/shareddi/src/nativeMain/kotlin/com/hedvig/android/shareddi/SharedModule.native.kt b/app/shareddi/src/nativeMain/kotlin/com/hedvig/android/shareddi/SharedModule.native.kt index f567bad381..ba189fa11b 100644 --- a/app/shareddi/src/nativeMain/kotlin/com/hedvig/android/shareddi/SharedModule.native.kt +++ b/app/shareddi/src/nativeMain/kotlin/com/hedvig/android/shareddi/SharedModule.native.kt @@ -1,9 +1,14 @@ package com.hedvig.android.shareddi -import com.apollographql.apollo.ApolloClient -import com.apollographql.apollo.network.http.DefaultHttpEngine +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.network.ktor3.KtorNetworkFetcherFactory +import com.hedvig.android.core.common.di.baseHttpClientQualifier import com.hedvig.android.core.datastore.DeviceIdFetcher +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.language.LanguageStorage import com.hedvig.android.network.clients.AccessTokenFetcher +import io.ktor.client.HttpClient import org.koin.core.module.Module import org.koin.dsl.module @@ -13,11 +18,29 @@ internal actual val platformModule: Module = module { /** * Like [platformModule] but allows for dynamic input, for pieces that need to be injected from iOS */ -internal fun iosPlatformModule(accessTokenFetcher: AccessTokenFetcher, deviceIdFetcher: DeviceIdFetcher) = module { +internal fun iosPlatformModule( + accessTokenFetcher: AccessTokenFetcher, + deviceIdFetcher: DeviceIdFetcher, + featureManager: FeatureManager, + languageStorage: LanguageStorage, +) = module { single { accessTokenFetcher } single { deviceIdFetcher } + single { + featureManager + } + single { + languageStorage + } + single { + ImageLoader.Builder(PlatformContext.INSTANCE) + .components { + add(KtorNetworkFetcherFactory(get(baseHttpClientQualifier))) + } + .build() + } } diff --git a/app/shareddi/src/nativeMain/kotlin/com/hedvig/android/shareddi/main.native.kt b/app/shareddi/src/nativeMain/kotlin/com/hedvig/android/shareddi/main.native.kt index 546fd8de09..f6d1bb21f8 100644 --- a/app/shareddi/src/nativeMain/kotlin/com/hedvig/android/shareddi/main.native.kt +++ b/app/shareddi/src/nativeMain/kotlin/com/hedvig/android/shareddi/main.native.kt @@ -1,7 +1,14 @@ package com.hedvig.android.shareddi import com.hedvig.android.core.buildconstants.AppBuildConfig +import com.hedvig.android.core.buildconstants.di.buildConstantsModule import com.hedvig.android.core.datastore.DeviceIdFetcher +import com.hedvig.android.core.datastore.di.dataStoreModule +import com.hedvig.android.data.conversations.di.dataConversationsModule +import com.hedvig.android.feature.help.center.di.helpCenterModule +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.language.LanguageStorage +import com.hedvig.android.language.di.languageModule import com.hedvig.android.network.clients.AccessTokenFetcher import com.hedvig.android.permission.di.noopPermissionModule import com.hedvig.feature.claim.chat.di.claimChatModule @@ -11,13 +18,19 @@ import org.koin.core.context.startKoin fun initKoin( accessTokenFetcher: AccessTokenFetcher, deviceIdFetcher: DeviceIdFetcher, + featureManager: FeatureManager, + languageStorage: LanguageStorage, appBuildConfig: AppBuildConfig, ) { startKoin { modules( - iosPlatformModule(accessTokenFetcher, deviceIdFetcher), + iosPlatformModule(accessTokenFetcher, deviceIdFetcher, featureManager, languageStorage), sharedModule(appBuildConfig), + dataStoreModule, + languageModule, + dataConversationsModule, claimChatModule, + helpCenterModule, noopPermissionModule, ) } diff --git a/app/ui/ui-emergency/build.gradle.kts b/app/ui/ui-emergency/build.gradle.kts index 43a1db82fe..a9cafd2b20 100644 --- a/app/ui/ui-emergency/build.gradle.kts +++ b/app/ui/ui-emergency/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("hedvig.android.library") + id("hedvig.multiplatform.library") id("hedvig.gradle.plugin") } @@ -8,14 +8,19 @@ hedvig { compose() } -dependencies { - implementation(libs.androidx.compose.foundation) - implementation(libs.androidx.compose.foundationLayout) - implementation(libs.jetbrains.compose.runtime) - implementation(libs.jetbrains.compose.ui) - implementation(libs.kotlinx.serialization.core) - implementation(projects.coreResources) - implementation(projects.coreUiData) - implementation(projects.dataContract) - implementation(projects.designSystemHedvig) +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.jetbrains.compose.foundation) + implementation(libs.jetbrains.compose.foundation.layout) + implementation(libs.jetbrains.compose.runtime) + implementation(libs.jetbrains.compose.ui) + implementation(libs.kotlinx.serialization.core) + implementation(projects.coreResources) + implementation(projects.coreUiData) + implementation(projects.dataContract) + implementation(projects.designSystemHedvig) + implementation(projects.loggingPublic) + } + } } diff --git a/app/ui/ui-emergency/src/main/kotlin/com/hedvig/android/ui/emergency/FirstVetScreen.kt b/app/ui/ui-emergency/src/commonMain/kotlin/com/hedvig/android/ui/emergency/FirstVetScreen.kt similarity index 95% rename from app/ui/ui-emergency/src/main/kotlin/com/hedvig/android/ui/emergency/FirstVetScreen.kt rename to app/ui/ui-emergency/src/commonMain/kotlin/com/hedvig/android/ui/emergency/FirstVetScreen.kt index f7c32fab00..f5509c5c9f 100644 --- a/app/ui/ui-emergency/src/main/kotlin/com/hedvig/android/ui/emergency/FirstVetScreen.kt +++ b/app/ui/ui-emergency/src/commonMain/kotlin/com/hedvig/android/ui/emergency/FirstVetScreen.kt @@ -1,7 +1,5 @@ package com.hedvig.android.ui.emergency -import android.content.Intent -import android.net.Uri import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -14,7 +12,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.semantics @@ -43,9 +40,9 @@ fun FirstVetScreen( sections: List, navigateUp: () -> Unit, navigateBack: () -> Unit, + openUrl: (String) -> Unit, modifier: Modifier = Modifier, ) { - val context = LocalContext.current HedvigScaffold( topAppBarText = stringResource(Res.string.HC_QUICK_ACTIONS_FIRSTVET_TITLE), navigateUp = navigateUp, @@ -98,9 +95,7 @@ fun FirstVetScreen( ?: stringResource(Res.string.SUBMIT_CLAIM_GLASS_DAMAGE_ONLINE_BOOKING_BUTTON), onClick = { val url = section.url ?: "https://firstvet-se.go.link/kHxkt" - context.startActivity( - Intent(Intent.ACTION_VIEW, Uri.parse(url)), - ) + openUrl(url) }, modifier = Modifier.fillMaxWidth(), ) @@ -137,6 +132,7 @@ private fun PreviewFirstVetScreen() { }, {}, {}, + {}, ) } } diff --git a/app/ui/ui-emergency/src/main/AndroidManifest.xml b/app/ui/ui-emergency/src/main/AndroidManifest.xml deleted file mode 100644 index 568741e54f..0000000000 --- a/app/ui/ui-emergency/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/app/umbrella/build.gradle.kts b/app/umbrella/build.gradle.kts index 74234899a8..d4819b3557 100644 --- a/app/umbrella/build.gradle.kts +++ b/app/umbrella/build.gradle.kts @@ -2,7 +2,10 @@ import org.gradle.api.internal.catalog.DelegatingProjectDependency import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework plugins { + alias(libs.plugins.composeKotlinCompilerGradlePlugin) + alias(libs.plugins.composeJetbrainsCompilerGradlePlugin) id("hedvig.multiplatform.library") + id("hedvig.multiplatform.library.android") id("hedvig.gradle.plugin") alias(libs.plugins.kmpNativeCoroutines) } @@ -15,18 +18,26 @@ kotlin { val xcf = XCFramework(frameworkName) val projectsToExport: List = listOf( projects.authlib, - projects.featureClaimChat, + projects.coreBuildConstants, + projects.coreDatastorePublic, + projects.designSystemApi, + projects.featureFlags, + projects.featureHelpCenter, + projects.languageCore, + projects.loggingPublic, + projects.networkClients, projects.shareddi, ) listOf( - iosX64(), iosArm64(), iosSimulatorArm64(), ).forEach { iosTarget -> iosTarget.binaries.framework { + isStatic = true for (projectToExport in projectsToExport) { export(projectToExport) } + binaryOption("bundleId", frameworkName) baseName = frameworkName xcf.add(this) } diff --git a/build-logic/convention/src/main/kotlin/KotlinMultiplatformAndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KotlinMultiplatformAndroidLibraryConventionPlugin.kt index 23a7f87d39..24e7e601cc 100644 --- a/build-logic/convention/src/main/kotlin/KotlinMultiplatformAndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KotlinMultiplatformAndroidLibraryConventionPlugin.kt @@ -52,7 +52,6 @@ private fun Project.configureKotlinAndroidMultiplatform() { project.configure { targets.withType(KotlinMultiplatformAndroidLibraryTarget::class.java) { - @Suppress("UnstableApiUsage") androidResources.enable = true compileSdk = libs.versions.compileSdkVersion.get().toInt() minSdk = libs.versions.minSdkVersion.get().toInt() @@ -73,8 +72,6 @@ private fun Project.configureKotlinAndroidMultiplatform() { } } configureAutomaticNamespace(path, namespace, { namespace = it }) - // https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-resources-setup.html#resources-in-the-androidlibrary-target - experimentalProperties["android.experimental.kmp.enableAndroidResources"] = true } } diff --git a/build-logic/convention/src/main/kotlin/KotlinMultiplatformLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KotlinMultiplatformLibraryConventionPlugin.kt index 8ffbd9db61..d3631f856c 100644 --- a/build-logic/convention/src/main/kotlin/KotlinMultiplatformLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KotlinMultiplatformLibraryConventionPlugin.kt @@ -37,7 +37,6 @@ private fun Project.configureKotlinMultiplatform() { } listOf( - iosX64(), iosArm64(), iosSimulatorArm64(), ) diff --git a/build-logic/convention/src/main/kotlin/com/hedvig/android/ConfigureKotlinCommonCompilerOptions.kt b/build-logic/convention/src/main/kotlin/com/hedvig/android/ConfigureKotlinCommonCompilerOptions.kt index 7ffa7db195..42d4210ba7 100644 --- a/build-logic/convention/src/main/kotlin/com/hedvig/android/ConfigureKotlinCommonCompilerOptions.kt +++ b/build-logic/convention/src/main/kotlin/com/hedvig/android/ConfigureKotlinCommonCompilerOptions.kt @@ -30,7 +30,6 @@ private fun commonFreeCompilerArgs(): List { return listOf( "-Xcontext-parameters", "-Xexpect-actual-classes", - "-Xwhen-guards", "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", "-opt-in=androidx.compose.animation.ExperimentalSharedTransitionApi", "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", diff --git a/gradle.properties b/gradle.properties index 3f95d650a4..a751673dbf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,4 +39,7 @@ android.dependency.excludeLibraryComponentsFromConstraints=true android.r8.strictFullModeForKeepRules=false android.r8.optimizedResourceShrinking=false android.builtInKotlin=false -android.newDsl=false \ No newline at end of file +android.newDsl=false + +kotlin.native.cacheKind.iosSimulatorArm64=none +kotlin.native.binary.forceNativeThreadStateForFunctions=org_jetbrains_skia_DirectContext__1nFlushAndSubmit;org_jetbrains_skia_Canvas__1nDrawPicture diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b9e5952f56..83104abf22 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,8 +20,8 @@ doctor = "0.12.1" easylauncher = "6.4.1" googleServices = "4.4.4" gradleDevelocity = "4.3.2" -kmpNativeCoroutines = "1.0.1" -kotlin = "2.3.10" +kmpNativeCoroutines = "1.0.2" +kotlin = "2.3.20" kotlinter = "5.4.2" ksp = "2.3.2" ktor = "3.4.0" @@ -33,10 +33,10 @@ squareSortDependencies = "0.16" # Other versions sorted alphabetically accompanist = "0.37.3" rive = "11.2.1" -androidx-activity-compose = "1.12.3" -androidx-activity-core = "1.12.3" +androidx-activity-compose = "1.13.0" +androidx-activity-core = "1.13.0" androidx-annotation = "1.9.1" -androidx-composeBom = "2026.01.01" +androidx-composeBom = "2026.04.01" androidx-datastore = "1.2.0" androidx-junit = "1.3.0" androidx-lifecycle = "2.10.0" @@ -44,27 +44,27 @@ androidx-navigation = "2.9.7" androidx-other-appCompat = "1.7.1" androidx-other-browser = "1.9.0" androidx-other-constraintLayout = "2.2.0" -androidx-other-core = "1.17.0" +androidx-other-core = "1.18.0" androidx-other-splashscreen = "1.2.0" androidx-other-startup = "1.2.0" androidx-other-workManager = "2.11.1" androidx-test = "1.7.0" androidx-testRunners = "1.7.0" -androidx-ui-alpha = "1.10.2" +androidx-ui-alpha = "1.10.6" androidxGraphicsShapes = "1.1.0" -jetbrains-animation = "1.10.0" -jetbrains-compose = "1.10.0" -jetbrains-navigationevent = "1.0.0" +jetbrains-animation = "1.11.0-rc01" +jetbrains-compose = "1.11.0-rc01" +jetbrains-navigationevent = "1.0.1" jetbrains-graphics = "1.0.0-alpha09" -jetbrains-lifecycle = "2.9.6" -jetbrains-material = "1.10.0" -jetbrains-material3 = "1.10.0-alpha05" -jetbrains-navigation = "2.9.1" +jetbrains-lifecycle = "2.10.0" +jetbrains-material = "1.11.0-rc01" +jetbrains-material3 = "1.11.0-alpha07" +jetbrains-navigation = "2.9.2" +jetbrainsMarkdown = "0.7.3" arrow = "2.2.1.1" assertK = "0.28.1" atomicfu = "0.31.0" coil = "3.3.0" -composeRichtext = "1.0.0-alpha03" coreLibraryDesugaring = "2.1.5" coroutines = "1.10.2" datadog = "3.6.0" @@ -74,8 +74,9 @@ firebaseCrashlyticsBuildtools = "3.0.6" junit = "4.13.2" koinBom = "4.1.1" kotlinx-serialization = "1.10.0" -kotlinxDatetime = "0.7.1" +kotlinxDatetime = "0.7.1-0.6.x-compat" media3 = "1.9.2" +mikepenzMarkdown = "0.40.2" modalSheet = "0.7.0" navigationRecentsUrlSharing = "1.0.0" okio = "3.16.4" @@ -147,9 +148,8 @@ coil-compose = { module = "io.coil-kt.coil3:coil-compose-core", version.ref = "c coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" } coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil" } -compose-richtext = { module = "com.halilibo.compose-richtext:richtext-ui", version.ref = "composeRichtext" } -compose-richtextCommonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "composeRichtext" } coreLibraryDesugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "coreLibraryDesugaring" } +mikepenz-markdown = { module = "com.mikepenz:multiplatform-markdown-renderer", version.ref = "mikepenzMarkdown" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } datadog-sdk-compose = { module = "com.datadoghq:dd-sdk-android-compose", version.ref = "datadog" } @@ -181,6 +181,7 @@ jetbrains-compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-t jetbrains-compose-ui-unit = { module = "org.jetbrains.compose.ui:ui-unit", version.ref = "jetbrains-compose" } jetbrains-compose-ui-util = { module = "org.jetbrains.compose.ui:ui-util", version.ref = "jetbrains-compose" } jetbrains-graphics-shapes = { module = "org.jetbrains.androidx.graphics:graphics-shapes", version.ref = "jetbrains-graphics" } +jetbrains-markdown = { module = "org.jetbrains:markdown", version.ref = "jetbrainsMarkdown" } jetbrains-lifecycle-common = { module = "org.jetbrains.androidx.lifecycle:lifecycle-common", version.ref = "jetbrains-lifecycle" } jetbrains-lifecycle-runtime = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime", version.ref = "jetbrains-lifecycle" } jetbrains-lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "jetbrains-lifecycle" } diff --git a/gradle/projectDependencyGraph.gradle b/gradle/projectDependencyGraph.gradle index 81c1678bf9..2235a196cf 100644 --- a/gradle/projectDependencyGraph.gradle +++ b/gradle/projectDependencyGraph.gradle @@ -53,7 +53,8 @@ task generateProjectDependencyGraph { project.configurations.all { config -> config.dependencies .withType(ProjectDependency) - .collect { it.dependencyProject } + .collect { rootProject.findProject(it.path) } + .findAll { it != null } .each { dependency -> projects.add(project) projects.add(dependency) diff --git a/lokalise-gradle-plugin/lokalise/src/main/kotlin/com/hedvig/android/lokalise/task/DownloadStringsTask.kt b/lokalise-gradle-plugin/lokalise/src/main/kotlin/com/hedvig/android/lokalise/task/DownloadStringsTask.kt index e2ced355fb..d533334e7a 100644 --- a/lokalise-gradle-plugin/lokalise/src/main/kotlin/com/hedvig/android/lokalise/task/DownloadStringsTask.kt +++ b/lokalise-gradle-plugin/lokalise/src/main/kotlin/com/hedvig/android/lokalise/task/DownloadStringsTask.kt @@ -16,7 +16,6 @@ import kotlinx.coroutines.runBlocking import java.net.URI import javax.inject.Inject import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.add import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject @@ -83,6 +82,7 @@ abstract class DownloadStringsTask @Inject constructor( .removeDotsFromStringIds() .convertSimpleFormatToNumberedFormat(resourcesType) .removeEscapesFromMultiplatformStrings(resourcesType) + .removeXmlVersionHeaderFromMultiplatformResources(resourcesType) .addUntranslatableStrings() } tempFileForZipFile.delete() @@ -116,10 +116,11 @@ abstract class DownloadStringsTask @Inject constructor( ) } logger.debug("{} asyncDownloadRequest body:{}", tag, requestBody) - val response = httpClient.post("https://api.lokalise.com/api2/projects/${lokaliseProjectId.get()}/files/async-download") { - commonLokaliseHeaders() - setBody(requestBody.toString()) - }.bodyAsText() + val response = + httpClient.post("https://api.lokalise.com/api2/projects/${lokaliseProjectId.get()}/files/async-download") { + commonLokaliseHeaders() + setBody(requestBody.toString()) + }.bodyAsText() logger.debug("{} post response:{}", tag, response) val processId = Json.parseToJsonElement(response).jsonObject["process_id"]?.jsonPrimitive?.content ?: error("Lokalise responded with a null processId") @@ -266,6 +267,19 @@ abstract class DownloadStringsTask @Inject constructor( } } } + + /** + * The multiplatform resources do not need this heading, and may also result in some weird runtime errors + */ + private fun String.removeXmlVersionHeaderFromMultiplatformResources(resourcesType: ResourcesType): String { + return when (resourcesType) { + ResourcesType.Android -> this + ResourcesType.KMP -> { + removePrefix("""""").trimStart() + } + } + } + private fun String.addUntranslatableStrings(): String { return this.replace( """""", @@ -275,7 +289,7 @@ abstract class DownloadStringsTask @Inject constructor( | English | Swish | Trustly - """.trimMargin("|") + """.trimMargin("|"), ) } diff --git a/micro-apps/umbrella-consumer/umbrella-consumer.xcodeproj/project.pbxproj b/micro-apps/umbrella-consumer/umbrella-consumer.xcodeproj/project.pbxproj index b5dc700d0b..5884840711 100644 --- a/micro-apps/umbrella-consumer/umbrella-consumer.xcodeproj/project.pbxproj +++ b/micro-apps/umbrella-consumer/umbrella-consumer.xcodeproj/project.pbxproj @@ -6,6 +6,18 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + 558DDBF52F76ACAE00B1F12F /* DatadogCore in Frameworks */ = {isa = PBXBuildFile; productRef = 558DDBF42F76ACAE00B1F12F /* DatadogCore */; }; + 558DDBF72F76ACAE00B1F12F /* DatadogCrashReporting in Frameworks */ = {isa = PBXBuildFile; productRef = 558DDBF62F76ACAE00B1F12F /* DatadogCrashReporting */; }; + 558DDBF92F76ACAE00B1F12F /* DatadogFlags in Frameworks */ = {isa = PBXBuildFile; productRef = 558DDBF82F76ACAE00B1F12F /* DatadogFlags */; }; + 558DDBFB2F76ACAE00B1F12F /* DatadogLogs in Frameworks */ = {isa = PBXBuildFile; productRef = 558DDBFA2F76ACAE00B1F12F /* DatadogLogs */; }; + 558DDBFD2F76ACAE00B1F12F /* DatadogProfiling in Frameworks */ = {isa = PBXBuildFile; productRef = 558DDBFC2F76ACAE00B1F12F /* DatadogProfiling */; }; + 558DDBFF2F76ACAE00B1F12F /* DatadogRUM in Frameworks */ = {isa = PBXBuildFile; productRef = 558DDBFE2F76ACAE00B1F12F /* DatadogRUM */; }; + 558DDC012F76ACAE00B1F12F /* DatadogSessionReplay in Frameworks */ = {isa = PBXBuildFile; productRef = 558DDC002F76ACAE00B1F12F /* DatadogSessionReplay */; }; + 558DDC032F76ACAE00B1F12F /* DatadogTrace in Frameworks */ = {isa = PBXBuildFile; productRef = 558DDC022F76ACAE00B1F12F /* DatadogTrace */; }; + 558DDC052F76ACAE00B1F12F /* DatadogWebViewTracking in Frameworks */ = {isa = PBXBuildFile; productRef = 558DDC042F76ACAE00B1F12F /* DatadogWebViewTracking */; }; +/* End PBXBuildFile section */ + /* Begin PBXFileReference section */ 55D8AB952EBE16C700E53763 /* umbrella-consumer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "umbrella-consumer.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -36,6 +48,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 558DDC052F76ACAE00B1F12F /* DatadogWebViewTracking in Frameworks */, + 558DDBFD2F76ACAE00B1F12F /* DatadogProfiling in Frameworks */, + 558DDBFB2F76ACAE00B1F12F /* DatadogLogs in Frameworks */, + 558DDBF72F76ACAE00B1F12F /* DatadogCrashReporting in Frameworks */, + 558DDC012F76ACAE00B1F12F /* DatadogSessionReplay in Frameworks */, + 558DDBF52F76ACAE00B1F12F /* DatadogCore in Frameworks */, + 558DDBFF2F76ACAE00B1F12F /* DatadogRUM in Frameworks */, + 558DDBF92F76ACAE00B1F12F /* DatadogFlags in Frameworks */, + 558DDC032F76ACAE00B1F12F /* DatadogTrace in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -79,6 +100,15 @@ ); name = "umbrella-consumer"; packageProductDependencies = ( + 558DDBF42F76ACAE00B1F12F /* DatadogCore */, + 558DDBF62F76ACAE00B1F12F /* DatadogCrashReporting */, + 558DDBF82F76ACAE00B1F12F /* DatadogFlags */, + 558DDBFA2F76ACAE00B1F12F /* DatadogLogs */, + 558DDBFC2F76ACAE00B1F12F /* DatadogProfiling */, + 558DDBFE2F76ACAE00B1F12F /* DatadogRUM */, + 558DDC002F76ACAE00B1F12F /* DatadogSessionReplay */, + 558DDC022F76ACAE00B1F12F /* DatadogTrace */, + 558DDC042F76ACAE00B1F12F /* DatadogWebViewTracking */, ); productName = "umbrella-consumer"; productReference = 55D8AB952EBE16C700E53763 /* umbrella-consumer.app */; @@ -108,6 +138,9 @@ ); mainGroup = 55D8AB8C2EBE16C700E53763; minimizedProjectReferenceProxies = 1; + packageReferences = ( + 558DDBF32F76ACAE00B1F12F /* XCRemoteSwiftPackageReference "dd-sdk-ios" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 55D8AB962EBE16C700E53763 /* Products */; projectDirPath = ""; @@ -145,7 +178,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd \"$SRCROOT\"\ncd ../..\nexport JAVA_HOME=\"/Library/Java/JavaVirtualMachines/zulu-23.jdk/Contents/Home\"\n./gradlew :umbrella:embedAndSignAppleFrameworkForXcode\n"; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT\"\ncd ../..\nexport JAVA_HOME=\"/Library/Java/JavaVirtualMachines/zulu-23.jdk/Contents/Home\"\n./gradlew :umbrella:embedAndSignAppleFrameworkForXcode\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -213,7 +246,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -271,7 +304,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 26.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -296,8 +329,9 @@ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -330,8 +364,9 @@ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -371,6 +406,65 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 558DDBF32F76ACAE00B1F12F /* XCRemoteSwiftPackageReference "dd-sdk-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/DataDog/dd-sdk-ios.git"; + requirement = { + kind = exactVersion; + version = 3.6.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 558DDBF42F76ACAE00B1F12F /* DatadogCore */ = { + isa = XCSwiftPackageProductDependency; + package = 558DDBF32F76ACAE00B1F12F /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogCore; + }; + 558DDBF62F76ACAE00B1F12F /* DatadogCrashReporting */ = { + isa = XCSwiftPackageProductDependency; + package = 558DDBF32F76ACAE00B1F12F /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogCrashReporting; + }; + 558DDBF82F76ACAE00B1F12F /* DatadogFlags */ = { + isa = XCSwiftPackageProductDependency; + package = 558DDBF32F76ACAE00B1F12F /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogFlags; + }; + 558DDBFA2F76ACAE00B1F12F /* DatadogLogs */ = { + isa = XCSwiftPackageProductDependency; + package = 558DDBF32F76ACAE00B1F12F /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogLogs; + }; + 558DDBFC2F76ACAE00B1F12F /* DatadogProfiling */ = { + isa = XCSwiftPackageProductDependency; + package = 558DDBF32F76ACAE00B1F12F /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogProfiling; + }; + 558DDBFE2F76ACAE00B1F12F /* DatadogRUM */ = { + isa = XCSwiftPackageProductDependency; + package = 558DDBF32F76ACAE00B1F12F /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogRUM; + }; + 558DDC002F76ACAE00B1F12F /* DatadogSessionReplay */ = { + isa = XCSwiftPackageProductDependency; + package = 558DDBF32F76ACAE00B1F12F /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogSessionReplay; + }; + 558DDC022F76ACAE00B1F12F /* DatadogTrace */ = { + isa = XCSwiftPackageProductDependency; + package = 558DDBF32F76ACAE00B1F12F /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogTrace; + }; + 558DDC042F76ACAE00B1F12F /* DatadogWebViewTracking */ = { + isa = XCSwiftPackageProductDependency; + package = 558DDBF32F76ACAE00B1F12F /* XCRemoteSwiftPackageReference "dd-sdk-ios" */; + productName = DatadogWebViewTracking; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 55D8AB8D2EBE16C700E53763 /* Project object */; } diff --git a/micro-apps/umbrella-consumer/umbrella-consumer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/micro-apps/umbrella-consumer/umbrella-consumer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000000..2d1eba0f77 --- /dev/null +++ b/micro-apps/umbrella-consumer/umbrella-consumer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,42 @@ +{ + "originHash" : "ecc3edec2d3d8492b2aafc6cd36445ca859b5fde20181e9e4a9c7d388015e111", + "pins" : [ + { + "identity" : "dd-sdk-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/DataDog/dd-sdk-ios.git", + "state" : { + "revision" : "8291deb7abd083a2432c6ec323960c495ebdc76c", + "version" : "3.6.1" + } + }, + { + "identity" : "kscrash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kstenerud/KSCrash.git", + "state" : { + "revision" : "95a8895d75f3c22aa9ad9f2a15d2fbd97b0a55e2", + "version" : "2.5.1" + } + }, + { + "identity" : "opentelemetry-swift-core", + "kind" : "remoteSourceControl", + "location" : "https://github.com/open-telemetry/opentelemetry-swift-core", + "state" : { + "revision" : "240c8d5e36c3c7b774ed961325369f0b1f2c965f", + "version" : "2.3.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" + } + } + ], + "version" : 3 +} diff --git a/micro-apps/umbrella-consumer/umbrella-consumer.xcodeproj/xcuserdata/stylianosgakis.xcuserdatad/xcschemes/umbrella-consumer.xcscheme b/micro-apps/umbrella-consumer/umbrella-consumer.xcodeproj/xcuserdata/stylianosgakis.xcuserdatad/xcschemes/umbrella-consumer.xcscheme index 83f08be795..98874b8677 100644 --- a/micro-apps/umbrella-consumer/umbrella-consumer.xcodeproj/xcuserdata/stylianosgakis.xcuserdatad/xcschemes/umbrella-consumer.xcscheme +++ b/micro-apps/umbrella-consumer/umbrella-consumer.xcodeproj/xcuserdata/stylianosgakis.xcuserdatad/xcschemes/umbrella-consumer.xcscheme @@ -1,16 +1,10 @@ - + version = "1.3"> + + buildForRunning = "YES"> - - - + - - - - - - diff --git a/micro-apps/umbrella-consumer/umbrella-consumer/ContentView.swift b/micro-apps/umbrella-consumer/umbrella-consumer/ContentView.swift index bac5b60fc8..8ef9cfbfce 100644 --- a/micro-apps/umbrella-consumer/umbrella-consumer/ContentView.swift +++ b/micro-apps/umbrella-consumer/umbrella-consumer/ContentView.swift @@ -2,21 +2,111 @@ import SwiftUI import HedvigShared struct ContentView: View { + @State private var presented: Destination? + + enum Destination: Identifiable { + case helpCenter + case puppyGuide + var id: Self { self } + } + + var body: some View { + NavigationStack { + List { + Button("Help Center") { presented = .helpCenter } + Button("Puppy Guide") { presented = .puppyGuide } + } + .navigationTitle("Umbrella consumer") + } + .fullScreenCover(item: $presented) { destination in + switch destination { + case .helpCenter: + HelpCenterScreen(onDismiss: { presented = nil }) + .ignoresSafeArea(.all) + case .puppyGuide: + PuppyGuideStack(onDismiss: { presented = nil }) + .ignoresSafeArea(.all) + } + } + } +} + +private struct HelpCenterScreen: UIViewControllerRepresentable { + let onDismiss: () -> Void + + func makeUIViewController(context: Context) -> UIViewController { + return HelpCenterViewControllerKt.HelpCenterViewController( + onNavigateUp: onDismiss, + onNavigateToInbox: {}, + onNavigateToNewConversation: {}, + onNavigateToQuickLink: { _ in }, + openUrl: { url in + if let nsUrl = URL(string: url) { + UIApplication.shared.open(nsUrl) + } + }, + tryToDialPhone: { number in + let cleaned = number.components(separatedBy: .whitespaces).joined() + if let url = URL(string: "tel://\(cleaned)") { + UIApplication.shared.open(url) + } + } + ) + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} + +private struct PuppyGuideStack: View { + let onDismiss: () -> Void + @State private var path: [String] = [] + var body: some View { - VStack { - ClaimChatViewController() + NavigationStack(path: $path) { + PuppyGuideScreen( + onNavigateUp: onDismiss, + onNavigateToArticle: { storyName in path.append(storyName) } + ) + .ignoresSafeArea(.all) + .toolbar(.hidden, for: .navigationBar) + .navigationDestination(for: String.self) { storyName in + PuppyArticleScreen( + storyName: storyName, + navigateUp: { if !path.isEmpty { path.removeLast() } } + ) + .ignoresSafeArea(.all) + .toolbar(.hidden, for: .navigationBar) + } } - .padding() } } -struct ClaimChatViewController: UIViewControllerRepresentable { +private struct PuppyGuideScreen: UIViewControllerRepresentable { + let onNavigateUp: () -> Void + let onNavigateToArticle: (String) -> Void + func makeUIViewController(context: Context) -> UIViewController { - return ClaimChatViewControllerKt.ClaimChatViewController() + return PuppyGuideViewControllersKt.PuppyGuideViewController( + onNavigateUp: onNavigateUp, + onNavigateToArticle: onNavigateToArticle + ) } - func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} + +private struct PuppyArticleScreen: UIViewControllerRepresentable { + let storyName: String + let navigateUp: () -> Void + + func makeUIViewController(context: Context) -> UIViewController { + return PuppyGuideViewControllersKt.PuppyArticleViewController( + storyName: storyName, + navigateUp: navigateUp + ) } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} } #Preview { diff --git a/micro-apps/umbrella-consumer/umbrella-consumer/IosInterfaces.swift b/micro-apps/umbrella-consumer/umbrella-consumer/IosInterfaces.swift new file mode 100644 index 0000000000..7b6142704d --- /dev/null +++ b/micro-apps/umbrella-consumer/umbrella-consumer/IosInterfaces.swift @@ -0,0 +1,67 @@ +import HedvigShared +import SwiftUI + +class IosAccessTokenFetcher: AccessTokenFetcher { + let keychainAbstraction: KeychainAbstraction + + init(_ keychainAbstraction: KeychainAbstraction) { + self.keychainAbstraction = keychainAbstraction + } + + func fetch() async throws -> String? { + return try await keychainAbstraction.getToken() + } +} + +protocol KeychainAbstraction { + func getToken() async throws -> String +} + +class IosKeychainAbstraction: KeychainAbstraction { + func getToken() async throws -> String { + try await Task.sleep(for: .seconds(1)) + return "eyJraWQiOiJCSnd5VGNnek5WUmpmX0VuZjFKUFgxd3lrUjZSMElOTXRiR015UkduVkhNIiwiYWxnIjoiUlMyNTYifQ.eyJpbXBlcnNvbmF0ZWQtYnkiOiJhZG1fNWNmOTA4ZjAtMGZhMi00ZGE0LWExNzAtMjcyNjQwNTc3MDVhIiwic3ViIjoibWVtXzMzMDY3MTM3NiIsImV4cCI6MTc3ODA4MzQ2NSwiaWF0IjoxNzc4MDc5ODY1fQ.Tw40qwWJ9P3C8zxUYB4EKPXEbE3UfeCvTvF0fDh5bkhl0g6kZLO847ePJiBYEq9IGg6UXLEJfKx7veBNnU5aSIAtVrftKv167IANvyZBIEJR20rYi4qU_eayLaaCkFbp2L-9bVb3ZWNbErHlF9-f1tnEddajm3WPlrJ_eXklLdnXfA3dX6JTAkY-D-O5NdgVcjMVFJWuKMEv72fpHL1XMM51jWuAE4f6-ezJH3yviynBzIdxMAYuOPo8weeMPN7hieCFF95AFY8quwN8K2T9a6rE4-U8lTKr88Q6YLq_OBAq3RfSoG22WBFRb1WYj4yK2L9er4PWtzVNj3KbCkUIhA" + } +} + +class IosDeviceIdFetcher: DeviceIdFetcher { + func fetch() async throws -> String? { + return UIDevice.current.identifierForVendor?.uuidString + } +} + +func iosFeatureManager() -> FeatureManager { + return IosFeatureManager( + isFeatureEnabledBlock: { feature in + switch feature { + default: false + } + } + ) +} + +class IosAppBuildConfig: AppBuildConfig { + var appFlavor: Flavor = Flavor.develop + + var applicationId: String = "" + + var brand: String = "" + + var buildType: String = "" + + var debug: Bool = true + + var device: String = "" + + var manufacturer: String = "" + + var model: String = "" + + var osReleaseVersion: String = "" + + var osSdkVersion: Int32 = 0 + + var versionCode: Int32 = 0 + + var versionName: String = "" +} diff --git a/micro-apps/umbrella-consumer/umbrella-consumer/umbrella_consumerApp.swift b/micro-apps/umbrella-consumer/umbrella-consumer/umbrella_consumerApp.swift index f89f5fe87b..28ce0416e8 100644 --- a/micro-apps/umbrella-consumer/umbrella-consumer/umbrella_consumerApp.swift +++ b/micro-apps/umbrella-consumer/umbrella-consumer/umbrella_consumerApp.swift @@ -3,36 +3,28 @@ import HedvigShared @main struct umbrella_consumerApp: App { - init() { - let keychain = IosKeychainAbstraction() - Main_nativeKt.doInitKoin(accessTokenFetcher: IosAccessTokenFetcher(keychain)) - } var body: some Scene { WindowGroup { ContentView() } } -} - -class IosAccessTokenFetcher: AccessTokenFetcher { - let keychainAbstraction: KeychainAbstraction - - init(_ keychainAbstraction: KeychainAbstraction) { - self.keychainAbstraction = keychainAbstraction - } - func fetch() async throws -> String? { - return try await keychainAbstraction.getToken() - } -} - -protocol KeychainAbstraction { - func getToken() async throws -> String + @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate } -class IosKeychainAbstraction: KeychainAbstraction { - func getToken() async throws -> String { - try await Task.sleep(for: .seconds(2)) - return "eyJraWQiOiJCSnd5VGNnek5WUmpmX0VuZjFKUFgxd3lrUjZSMElOTXRiR015UkduVkhNIiwiYWxnIjoiUlMyNTYifQ.eyJpbXBlcnNvbmF0ZWQtYnkiOiJhZG1fNWNmOTA4ZjAtMGZhMi00ZGE0LWExNzAtMjcyNjQwNTc3MDVhIiwic3ViIjoibWVtXzQ1NTQ2OTk5NyIsImV4cCI6MTc2Mjc4NDQyMiwiaWF0IjoxNzYyNzgwODIyfQ.OiSA4vQvWUunCV91RkwyW7zbsjQNjjl-SzTavkDLHr8m_xlZzAnnawHvssp8Lbhx880rSVJi-fE-akrZUYucnvlrP99Tht-G6L_igoeIIG66yFrvyUV6UAtqEyENJFptr_p-7BN-M5zkWuy1y3cunJDUXS0GGj8xsQ9CF9RAu0XkzVa-9Qeo7c2Ya73rHylR8yV8_AVaZ9fVl8bgeJGBnjjK_QT8Nbi7EuS2BKfB4TGPFp2phJnHu1Xio4Nx1CKNt4MiAsm2YYF8apEaquajyDngsstvjcUrnRU2ajmvY5s8UN4ErMAswSgKd_XondVZ_OokJHNTDXIE2Kj-StOYig" +class AppDelegate: UIResponder, UIApplicationDelegate { + // Run initializers on app launch + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + Main_nativeKt.doInitKoin( + accessTokenFetcher: IosAccessTokenFetcher(IosKeychainAbstraction()), + deviceIdFetcher: IosDeviceIdFetcher(), + featureManager: iosFeatureManager(), + appBuildConfig: IosAppBuildConfig(), + ) + IosLogcatLogger.companion.install() + return true } } diff --git a/misc/images/modularization-graph.png b/misc/images/modularization-graph.png index 4cedb36ef2..001bde35da 100644 Binary files a/misc/images/modularization-graph.png and b/misc/images/modularization-graph.png differ