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 12d933cf63..25752d5b09 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 @@ -198,19 +198,6 @@ input BrowserInfo { timeZoneOffset: Int! javaEnabled: Boolean! } -""" -Yearly savings from bundle discount -""" -type BundleYearlySavings { - """ - Total yearly savings amount from bundle discount - """ - amount: Money! - """ - Whether the bundle discount covers the full period (true) or has pending/future items (false) - """ - bundleDiscountCoversFullPeriod: Boolean! -} type CampaignDiscount { """ The type of discount, determines the relevancy of the other fields. @@ -289,10 +276,6 @@ type CartBundleDiscountInfo { Number of products in cart that are eligible for bundle discount """ eligibleProductCount: Int - """ - Yearly savings from bundle discount - """ - yearlySavings: BundleYearlySavings! } type CartCost { """ @@ -635,6 +618,7 @@ Root type expressing the entire flow for members trying to report a new claim. type ClaimIntent { id: ID! currentStep: ClaimIntentStep! + sourceMessages: [ClaimIntentSourceMessage!] } input ClaimIntentFormSubmitInputField { fieldId: ID! @@ -644,6 +628,17 @@ type ClaimIntentMutationOutput { intent: ClaimIntent userError: UserError } +type ClaimIntentSourceMessage { + id: ID! + text: String! +} +input ClaimIntentStartInput { + """ + Optionally inject a chat message ID to start the claim intent from. This will replace the audio recording + as the first step, using the chat history as the implicit first step. + """ + sourceMessageId: ID +} """ Represents a single step in the claim submission flow. Steps have some universally shared properties, as well as variable `content` which comes in many different @@ -657,7 +652,7 @@ type ClaimIntentStep { """ A union of all the different kinds of "step content". """ -union ClaimIntentStepContent = ClaimIntentStepContentForm|ClaimIntentStepContentTask|ClaimIntentStepContentAudioRecording|ClaimIntentStepContentSummary +union ClaimIntentStepContent = ClaimIntentStepContentForm|ClaimIntentStepContentTask|ClaimIntentStepContentAudioRecording|ClaimIntentStepContentSummary|ClaimIntentStepContentOutcome """ An audio recording step is one where the user is meant to record some audio. Submitted using `Mutation.claimIntentSubmitAudio`. @@ -735,6 +730,13 @@ This typically will be backed by a String - but other formats could appear. """ scalar ClaimIntentStepContentFormFieldValue """ +A step that shows the outcome of a claim intent. +This is a terminal step - there is no way to submit it. +""" +type ClaimIntentStepContentOutcome { + claimId: ID! +} +""" A read-only step where the entire claim intent information is displayed before submitting. Submitted using `Mutation.claimIntentSubmitSummary`. """ @@ -748,6 +750,8 @@ type ClaimIntentStepContentSummaryAudioRecording { } type ClaimIntentStepContentSummaryFileUpload { url: Url! + contentType: String! + fileName: String! } type ClaimIntentStepContentSummaryItem { title: String! @@ -1893,6 +1897,8 @@ type Member { claims: [Claim!]! claimsActive: [Claim!]! claimsHistory: [Claim!]! + partnerClaimsActive: [PartnerClaim!]! + partnerClaimsHistory: [PartnerClaim!]! firstName: String! lastName: String! ssn: String @@ -1949,6 +1955,11 @@ type Member { """ crossSellV2(input: CrossSellInput!): CrossSellV2! """ + Young Pet Guide stories for the member. + Returns a list of educational content stories for young pet owners. + """ + puppyGuideStories: [PuppyGuideStory!]! + """ Fetch all the active contracts for this member. Active contracts include all insurances that are either active today, or to-be-active in the future. """ @@ -2137,6 +2148,7 @@ input MemberLogDeviceInput { os: String! brand: String! model: String! + pushNotificationEnabled: Boolean } """ Container for mutations that refer to the currently authenticated member. @@ -2694,7 +2706,7 @@ type Mutation { """ Create or (reuse and existing) unsubmitted claim intent. """ - claimIntentStart: ClaimIntent! + claimIntentStart(input: ClaimIntentStartInput): ClaimIntent! """ Submit a step containing a `ClaimIntentStepContentForm`. """ @@ -2822,6 +2834,10 @@ type Mutation { """ productOfferAddonsSelect(productOfferId: UUID!, addonIds: [UUID!]!): ProductOffersMutationOutput! """ + Mark a young pet guide story as read for a specific member. + """ + puppyGuideEngagement(engagement: PuppyEngagementInput!): PuppyGuideStoryMutationOutput! + """ Update the customer of the shop session. Only non-null fields will be changed. Can trigger automatic lookup of other information. The session can be placed in a "point of no return" state where it is no longer legal to update the customer, @@ -2900,6 +2916,22 @@ type Mutation { """ upsellTravelAddonActivate(quoteId: ID!, addonId: ID!): UpsellTravelAddonActivationOutput! } +type PartnerClaim { + id: ID! + externalId: String! + exposureDisplayName: String + status: ClaimStatus + submittedAt: Date! + payoutAmount: Money + associatedTypeOfContract: String + claimType: String + handlerEmail: String + displayItems: [ClaimDisplayItem!]! + """ + Terms & conditions for the claim found using claims contractId and dateOfOccurrence, otherwise null. + """ + productVariant: ProductVariant +} type PartnerData { sas: SasPartnerData } @@ -3405,6 +3437,53 @@ type ProductVariantComparisonRow { """ covered: [String!]! } +input PuppyEngagementInput { + name: String! + rating: Int + opened: Boolean + read: Boolean + closed: Boolean +} +type PuppyGuideStory { + """ + The unique name/identifier of the story. + """ + name: String! + """ + The display title of the story. + """ + title: String! + """ + The subtitle or description of the story. + """ + subtitle: String! + """ + The main content of the story. + """ + content: String! + """ + The image associated with this story. + """ + image: String! + """ + Categories this story belongs to. + """ + categories: [String!]! + """ + The date when the story was marked as read by the user. + """ + read: Boolean! + """ + The user's rating of the story. + """ + rating: Int +} +type PuppyGuideStoryMutationOutput { + """ + Indicates whether the mutation was successful. + """ + success: Boolean! +} type Query { """ Return a conversation for a given ID. @@ -3412,6 +3491,7 @@ type Query { """ conversation(id: UUID!): Conversation claim(id: ID!): Claim + partnerClaim(id: ID!): PartnerClaim claimIntent(id: ID!): ClaimIntent! personalInformation(input: PersonalInformationInput!): PersonalInformation """ 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 d6d4424d29..e619d85f11 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 @@ -405,6 +405,7 @@ internal fun HedvigNavHost( navigateToInbox(backStackEntry) }, openUrl = openUrl, + imageLoader = imageLoader, ) imageViewerGraph(hedvigAppState.navController, imageLoader) } diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/ui/NavigationSuite.kt b/app/app/src/main/kotlin/com/hedvig/android/app/ui/NavigationSuite.kt index 551a4f9dde..c99c32aa89 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/ui/NavigationSuite.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/ui/NavigationSuite.kt @@ -39,8 +39,8 @@ internal fun NavigationSuite( .fillMaxWidth(), ) { AnimatedVisibility( - visible = navigationSuiteType == NavigationSuiteType.NavigationRail - || navigationSuiteType == NavigationSuiteType.NavigationRailXLarge, + visible = navigationSuiteType == NavigationSuiteType.NavigationRail || + navigationSuiteType == NavigationSuiteType.NavigationRailXLarge, enter = expandHorizontally(expandFrom = Alignment.End), exit = shrinkHorizontally(shrinkTowards = Alignment.End), ) { diff --git a/app/core/core-resources/src/main/res/values-sv-rSE/strings.xml b/app/core/core-resources/src/main/res/values-sv-rSE/strings.xml index 80040470e5..42a07d34c2 100644 --- a/app/core/core-resources/src/main/res/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/main/res/values-sv-rSE/strings.xml @@ -184,6 +184,7 @@ Gör röstinspelning Beskriv i text Din skadeanmälan + Händelsedatum Vilken försäkring gäller det? Vad gäller ditt ärende? Berätta vad som hände @@ -399,6 +400,7 @@ Invalid National Identity Number Vi försöker reparera i första hand, men om din %1$s skulle behöva ersättas helt (ex. om den blivit stulen) ersätts du med **%2$d\u0025** av inköpspriset **%3$d kr**, alltså **%4$d kr**. Värdering + Fråga angående skadeanmälan - Fordon reg. %1$s Välj land och språk Logga ut Få ett prisförslag @@ -525,6 +527,15 @@ Koppla autogiro Reseintyg Din profil + Se guider + I valpguiden hittar du användbara artiklar som hjälper dig med allt från första veterinärbesöket till hur du väljer rätt foder. + Utvalda guider + Läst + Inte hjälpsam + Var den här artikeln hjälpsam? + Mycket hjälpsam + Hjälpsamma guider för dig och din valp + Valpguiden Inte nu Slå på Läs mer @@ -824,6 +835,7 @@ Stängt Dina tidigare skador visas här automatiskt efter att de har hanterats. Ingen skadehistorik + Ditt skadeärende har stängts. Ditt skadeärende har stängts eftersom vi inte har fått någon återkoppling från dig. Se konversation för mer information. Din skadeanmälan granskas av en av våra försäkringsspecialister. Vi hör av oss snart med en uppdatering. Vi har återöppnat din skadeanmälan och en av våra försäkringsspecialister granskar den. Vi hör av oss snart med en uppdatering. @@ -853,6 +865,7 @@ Kvitto saknas Inskickat Uppladdade filer + Ta kontakt med skadeteamet Om du avslutar går du tillbaka till startsidan och dina inmatade uppgifter kommer att gå förlorade. Fortsätt Edit diff --git a/app/core/core-resources/src/main/res/values/strings.xml b/app/core/core-resources/src/main/res/values/strings.xml index 73a2be60fc..d062a31992 100644 --- a/app/core/core-resources/src/main/res/values/strings.xml +++ b/app/core/core-resources/src/main/res/values/strings.xml @@ -184,6 +184,7 @@ Use voice recording Describe using text Your claim + Date of occurrence What insurance is it about? What does your claim concern? Tell us what happened @@ -399,6 +400,7 @@ Invalid National Identity Number We first try to repair your %1$s, but if it needs to be replaced (e.g. if it was stolen) you will be compensated **%2$d\u0025** of the purchase price **%3$d SEK**, i.e **%4$d SEK**. Valuation + Question regarding claim, Vehicle reg. %1$s Preferences Logout Get a price quote @@ -525,6 +527,15 @@ Connect payment Travel certificates Your profile + Go to guides + In the puppy guide, you’ll find helpful articles covering everything from the first vet visit to choosing the right food. + Featured guides + Read + Not helpful + Was this article helpful? + Very helpful + Helpful guides for you and your puppy + Puppy Guide Not now Activate More info @@ -824,6 +835,7 @@ Closed Your past claims will appear here automatically once processed. No claims in history + Your claim was closed. Your claim was closed as we didn’t hear back from you. Please see conversation for more details. Your claim is being reviewed by one of our insurance specialists. We\'ll get back to you soon with an update. We have reopened your claim and one of our insurance specialists is reviewing it. We\'ll get back to you soon with an update. @@ -853,6 +865,7 @@ Receipt missing Submitted Uploaded files + Contact the claims team If you exit, you\'ll go back to the start page and your inputted data will be lost. Continue Edit diff --git a/app/feature/feature-help-center/build.gradle.kts b/app/feature/feature-help-center/build.gradle.kts index d826d9cfc2..e6e0f5df41 100644 --- a/app/feature/feature-help-center/build.gradle.kts +++ b/app/feature/feature-help-center/build.gradle.kts @@ -12,14 +12,17 @@ hedvig { dependencies { api(libs.androidx.navigation.common) + api(libs.coil.coil) implementation(libs.androidx.activity.compose) implementation(libs.androidx.compose.foundation) implementation(libs.androidx.lifecycle.compose) implementation(libs.androidx.navigation.compose) implementation(libs.apollo.runtime) + implementation(libs.apollo.normalizedCache) implementation(libs.arrow.core) implementation(libs.arrow.fx) + implementation(libs.coil.compose) implementation(libs.compose.richtext) implementation(libs.compose.richtextCommonmark) implementation(libs.coroutines.core) diff --git a/app/feature/feature-help-center/src/main/graphql/MutationPuppyGuideEngagement.graphql b/app/feature/feature-help-center/src/main/graphql/MutationPuppyGuideEngagement.graphql new file mode 100644 index 0000000000..0964b67a59 --- /dev/null +++ b/app/feature/feature-help-center/src/main/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/QueryPuppyGuide.graphql b/app/feature/feature-help-center/src/main/graphql/QueryPuppyGuide.graphql new file mode 100644 index 0000000000..0c4c92ea65 --- /dev/null +++ b/app/feature/feature-help-center/src/main/graphql/QueryPuppyGuide.graphql @@ -0,0 +1,14 @@ +query PuppyGuide { + currentMember { + puppyGuideStories { + 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/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterGraph.kt index 7b200246a4..c6b2decc90 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/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterGraph.kt @@ -2,6 +2,7 @@ package com.hedvig.android.feature.help.center import androidx.navigation.NavBackStackEntry import androidx.navigation.NavGraphBuilder +import coil.ImageLoader import com.hedvig.android.feature.help.center.commonclaim.FirstVetDestination import com.hedvig.android.feature.help.center.commonclaim.emergency.EmergencyDestination import com.hedvig.android.feature.help.center.data.InnerHelpCenterDestination @@ -12,6 +13,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 @@ -31,6 +36,7 @@ fun NavGraphBuilder.helpCenterGraph( onNavigateToInbox: (NavBackStackEntry) -> Unit, onNavigateToNewConversation: (NavBackStackEntry) -> Unit, openUrl: (String) -> Unit, + imageLoader: ImageLoader, ) { navgraph( startDestination = HelpCenterDestinations.HelpCenter::class, @@ -83,6 +89,13 @@ fun NavGraphBuilder.helpCenterGraph( onNavigateToNewConversation(backStackEntry) }, onNavigateUp = navigator::navigateUp, + onNavigateToPuppyGuide = { + with(navigator) { + backStackEntry.navigate( + HelpCenterDestinations.PuppyGuide, + ) + } + }, ) } @@ -139,6 +152,35 @@ fun NavGraphBuilder.helpCenterGraph( openUrl = openUrl, ) } + + navdestination { backStackEntry -> + val viewModel = koinViewModel() + PuppyGuideDestination( + viewModel, + onNavigateUp = navigator::navigateUp, + onNavigateToArticle = { story -> + with(navigator) { + backStackEntry.navigate( + HelpCenterDestinations.PuppyGuideArticle( + story.name, + ), + ) + } + }, + imageLoader = imageLoader, + ) + } + + navdestination { + val viewModel = koinViewModel { + parametersOf(storyName) + } + PuppyArticleDestination( + viewModel = viewModel, + navigateUp = navigator::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/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt index 8d20b0d420..14ae7877e9 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/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterPresenter.kt @@ -24,7 +24,9 @@ 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.PuppyGuideStory import com.hedvig.android.feature.help.center.data.QuickLinkDestination import com.hedvig.android.feature.help.center.model.QuickAction import com.hedvig.android.molecule.public.MoleculePresenter @@ -61,6 +63,7 @@ internal data class HelpCenterUiState( val search: Search?, val showNavigateToInboxButton: Boolean, val destinationToNavigate: QuickLinkDestination? = null, + val puppyGuide: List?, ) { data class QuickLink(val quickAction: QuickAction) @@ -93,6 +96,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 { @@ -152,7 +156,8 @@ internal class HelpCenterPresenter( combine( flow = flow { emit(getQuickLinksUseCase.invoke()) }, flow2 = flow { emit(getHelpCenterFAQUseCase.invoke()) }, - ) { quickLinks, faq -> + flow3 = flow { emit(getPuppyGuideUseCase.invoke()) }, + ) { quickLinks, faq, puppyGuideResult -> quickLinksUiState = quickLinks.fold( ifLeft = { HelpCenterUiState.QuickLinkUiState.NoQuickLinks @@ -170,12 +175,14 @@ internal class HelpCenterPresenter( ) val topics = faq.getOrNull()?.topics ?: listOf() val questions = faq.getOrNull()?.commonFAQ ?: listOf() + val puppyGuide = puppyGuideResult.getOrNull() currentState = currentState.copy( topics = topics, questions = questions, quickLinksUiState = quickLinksUiState, selectedQuickAction = selectedQuickAction, showNavigateToInboxButton = hasAnyActiveConversation, + puppyGuide = puppyGuide, ) }.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/main/kotlin/com/hedvig/android/feature/help/center/HelpCenterViewModel.kt index 8c4dd9354a..dffc05c82e 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/main/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.android.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/data/GetPuppyGuideUseCase.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetPuppyGuideUseCase.kt new file mode 100644 index 0000000000..03fe5387fa --- /dev/null +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/GetPuppyGuideUseCase.kt @@ -0,0 +1,61 @@ +package com.hedvig.android.feature.help.center.data + +import arrow.core.Either +import arrow.core.raise.either +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.cache.normalized.FetchPolicy +import com.apollographql.apollo.cache.normalized.fetchPolicy +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.logger.logcat +import kotlinx.serialization.Serializable +import octopus.PuppyGuideQuery + +internal interface GetPuppyGuideUseCase { + suspend fun invoke(): Either?> +} + +internal class GetPuppyGuideUseCaseImpl( + private val apolloClient: ApolloClient, +) : GetPuppyGuideUseCase { + override suspend fun invoke(): Either?> { + return either { + apolloClient + .query(PuppyGuideQuery()) + .fetchPolicy(FetchPolicy.NetworkOnly) + .safeExecute() + .onLeft { logcat { "Cannot load PuppyGuideStory: $it" } } + .getOrNull() + ?.currentMember + ?.puppyGuideStories + ?.map { story -> + // todo: remove log + if (story.name == "Ögonvård") { + logcat { "Mariia: story Ögonvård read or not: ${story.read} rating: ${story.rating} " } + } + 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, + ) + } + } + } +} + +@Serializable +internal 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/SetArticleRatingUseCase.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/SetArticleRatingUseCase.kt new file mode 100644 index 0000000000..776bdfb3dd --- /dev/null +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/data/SetArticleRatingUseCase.kt @@ -0,0 +1,35 @@ +package com.hedvig.android.feature.help.center.data + +import arrow.core.Either +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 com.hedvig.android.logger.logcat +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 apolloClient + .mutation( + PuppyGuideEngagementMutation( + name = articleName, + rating = Optional.present(rating), + ), + ) + .safeExecute() + .mapLeft { _ -> ErrorMessage() } + .onRight { data -> + logcat { "Mariia. Rating $rating for story $articleName set successfully" } + } + } +} 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/main/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt index 053df4eaa4..b09b7cc361 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/main/kotlin/com/hedvig/android/feature/help/center/di/HelpCenterModule.kt @@ -14,7 +14,13 @@ 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.SetArticleRatingUseCase +import com.hedvig.android.feature.help.center.data.SetArticleRatingUseCaseImpl +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,6 +37,10 @@ val helpCenterModule = module { GetHelpCenterTopicUseCaseImpl(get()) } + single { + GetPuppyGuideUseCaseImpl(get()) + } + single { GetQuickLinksUseCase( apolloClient = get(), @@ -54,6 +64,7 @@ val helpCenterModule = module { getQuickLinksUseCase = get(), hasAnyActiveConversationUseCase = get(), getHelpCenterFAQUseCase = get(), + getPuppyGuideUseCase = get(), ) } @@ -83,4 +94,20 @@ val helpCenterModule = module { hasAnyActiveConversationUseCase = get(), ) } + + viewModel { + PuppyGuideViewModel(getPuppyGuideUseCase = get()) + } + + viewModel { params -> + PuppyArticleViewModel( + getPuppyGuideUseCase = get(), + setArticleRatingUseCase = get(), + storyName = params.get(), + ) + } + + single { + SetArticleRatingUseCaseImpl(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/main/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt index 3f1cf5f01a..04796f2df9 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/main/kotlin/com/hedvig/android/feature/help/center/home/HelpCenterHomeDestination.kt @@ -15,6 +15,7 @@ import androidx.compose.animation.shrinkVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -46,10 +47,13 @@ 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.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -71,6 +75,7 @@ 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 @@ -78,6 +83,8 @@ 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 @@ -99,6 +106,7 @@ import com.hedvig.android.feature.help.center.HelpCenterUiState 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 +import com.hedvig.android.feature.help.center.data.PuppyGuideStory import com.hedvig.android.feature.help.center.data.QuickLinkDestination import com.hedvig.android.feature.help.center.model.QuickAction import com.hedvig.android.feature.help.center.model.QuickAction.MultiSelectExpandedLink @@ -118,6 +126,7 @@ internal fun HelpCenterHomeDestination( onNavigateUp: () -> Unit, onNavigateToInbox: () -> Unit, onNavigateToNewConversation: () -> Unit, + onNavigateToPuppyGuide: () -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(uiState.destinationToNavigate) { @@ -157,6 +166,8 @@ internal fun HelpCenterHomeDestination( reload = { viewModel.emit(HelpCenterEvent.ReloadFAQAndQuickLinks) }, + puppyGuide = uiState.puppyGuide, + onNavigateToPuppyGuide = onNavigateToPuppyGuide, ) } @@ -165,6 +176,7 @@ private fun HelpCenterHomeScreen( search: HelpCenterUiState.Search?, topics: List, questions: List, + puppyGuide: List?, quickLinksUiState: HelpCenterUiState.QuickLinkUiState, selectedQuickAction: QuickAction?, onNavigateToTopic: (topicId: String) -> Unit, @@ -179,6 +191,7 @@ private fun HelpCenterHomeScreen( onUpdateSearchResults: (String, HelpCenterUiState.HelpSearchResults?) -> Unit, onClearSearch: () -> Unit, reload: () -> Unit, + onNavigateToPuppyGuide: () -> Unit, ) { when (selectedQuickAction) { is StandaloneQuickLink -> { @@ -326,6 +339,8 @@ private fun HelpCenterHomeScreen( showNavigateToInboxButton = showNavigateToInboxButton, onNavigateToInbox = onNavigateToInbox, onNavigateToNewConversation = onNavigateToNewConversation, + puppyGuide = puppyGuide, + onNavigateToPuppyGuide = onNavigateToPuppyGuide, ) } else { SearchResults( @@ -352,10 +367,12 @@ private fun ContentWithoutSearch( topics: List, onNavigateToTopic: (topicId: String) -> Unit, questions: List, + puppyGuide: List?, onNavigateToQuestion: (questionId: String) -> Unit, showNavigateToInboxButton: Boolean, onNavigateToInbox: () -> Unit, onNavigateToNewConversation: () -> Unit, + onNavigateToPuppyGuide: () -> Unit, ) { Column { Column( @@ -363,13 +380,29 @@ private fun ContentWithoutSearch( Modifier.padding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal).asPaddingValues()), ) { Spacer(Modifier.height(32.dp)) - Image( - painter = painterResource(id = R.drawable.pillow_hedvig), - contentDescription = null, - modifier = Modifier - .size(170.dp) - .align(Alignment.CenterHorizontally), - ) + AnimatedContent( + puppyGuide != null, + contentAlignment = Alignment.Center, + ) { puppyGuideAvailable -> + Column( + Modifier.fillMaxWidth(), + ) { + if (puppyGuideAvailable) { + PuppyGuideCard( + onClick = onNavigateToPuppyGuide, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } else { + Image( + painter = painterResource(id = R.drawable.pillow_hedvig), + contentDescription = null, + modifier = Modifier + .size(170.dp) + .align(Alignment.CenterHorizontally), + ) + } + } + } Spacer(Modifier.height(50.dp)) Column( verticalArrangement = Arrangement.spacedBy(8.dp), @@ -445,6 +478,56 @@ private fun ContentWithoutSearch( } } +@Composable +private fun PuppyGuideCard(onClick: () -> Unit, modifier: Modifier = Modifier) { + HedvigCard( + color = HedvigTheme.colorScheme.backgroundPrimary, + modifier = modifier + .fillMaxWidth() + .shadow(1.dp, HedvigTheme.shapes.cornerXLarge) + .clickable(enabled = true) { + onClick() + }, + ) { + Column { + Box(Modifier.align(Alignment.CenterHorizontally)) { + Image( + painter = painterResource(id = com.hedvig.android.feature.help.center.R.drawable.hundar_badar_pet), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .height(182.dp) + .clip(HedvigTheme.shapes.cornerXLargeTop), + ) + HighlightLabel( + stringResource(R.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(R.string.PUPPY_GUIDE_TITLE), + modifier = Modifier.padding(horizontal = 16.dp), + ) + HedvigText( + stringResource(R.string.PUPPY_GUIDE_SUBTITLE), + modifier = Modifier.padding(horizontal = 16.dp), + color = HedvigTheme.colorScheme.textSecondary, + ) + Spacer(Modifier.height(16.dp)) + HedvigButtonGhostWithBorder( + stringResource(R.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, @@ -806,6 +889,19 @@ private fun PreviewHelpCenterHomeScreen( onUpdateSearchResults = { _, _ -> }, search = null, reload = {}, + puppyGuide = listOf( + PuppyGuideStory( + categories = listOf("Food"), + content = "some content", + image = "", + name = "", + rating = 5, + isRead = false, + subtitle = "Subtitle", + title = "Title", + ), + ), + onNavigateToPuppyGuide = {}, ) } } @@ -851,6 +947,8 @@ private fun PreviewQuickLinkAnimations() { onUpdateSearchResults = { _, _ -> }, search = null, reload = {}, + puppyGuide = null, + onNavigateToPuppyGuide = {}, ) } } @@ -880,6 +978,8 @@ private fun PreviewQuickLinkEmptyState() { onUpdateSearchResults = { _, _ -> }, search = null, reload = {}, + puppyGuide = null, + onNavigateToPuppyGuide = {}, ) } } 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/main/kotlin/com/hedvig/android/feature/help/center/navigation/HelpCenterDestination.kt index c8f824a270..9f7a9eabd0 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/main/kotlin/com/hedvig/android/feature/help/center/navigation/HelpCenterDestination.kt @@ -1,5 +1,6 @@ package com.hedvig.android.feature.help.center.navigation +import com.hedvig.android.feature.help.center.data.PuppyGuideStory import com.hedvig.android.navigation.common.Destination import com.hedvig.android.navigation.common.DestinationNavTypeAware import com.hedvig.android.ui.emergency.FirstVetSection @@ -43,6 +44,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/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleDestination.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleDestination.kt new file mode 100644 index 0000000000..2ae491e381 --- /dev/null +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleDestination.kt @@ -0,0 +1,303 @@ +package com.hedvig.android.feature.help.center.puppyguide + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.res.stringResource +import androidx.compose.ui.text.SpanStyle +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 coil.ImageLoader +import coil.compose.AsyncImage +import com.halilibo.richtext.commonmark.Markdown +import com.halilibo.richtext.ui.RichTextStyle +import com.halilibo.richtext.ui.string.RichTextStringStyle +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.HedvigScaffold +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.RichText +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.TopAppBarWithBack +import com.hedvig.android.design.system.hedvig.rememberPreviewImageLoader +import com.hedvig.android.feature.help.center.data.PuppyGuideStory +import com.hedvig.android.logger.logcat +import hedvig.resources.R + +@Composable +internal fun PuppyArticleDestination( + viewModel: PuppyArticleViewModel, + navigateUp: () -> Unit, + imageLoader: ImageLoader, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + PuppyArticleScreen( + uiState, + navigateUp = navigateUp, + onReload = { + viewModel.emit(PuppyArticleEvent.Reload) + }, + imageLoader = imageLoader, + onRatingClick = { + viewModel.emit(PuppyArticleEvent.RatingClick(it)) + }, + ) +} + +@Composable +private fun PuppyArticleScreen( + uiState: PuppyArticleUiState, + navigateUp: () -> Unit, + onReload: () -> Unit, + onRatingClick: (Int) -> Unit, + imageLoader: ImageLoader, +) { + when (uiState) { + PuppyArticleUiState.Failure -> HedvigScaffold( + navigateUp = navigateUp, + ) { + HedvigErrorSection( + onButtonClick = onReload, + modifier = Modifier.weight(1f), + ) + } + + PuppyArticleUiState.Loading -> HedvigFullScreenCenterAlignedProgress() + + is PuppyArticleUiState.Success -> PuppyArticleSuccessScreen( + uiState, + navigateUp = navigateUp, + imageLoader = imageLoader, + onRatingClick = onRatingClick, + ) + } +} + +@Composable +private fun PuppyArticleSuccessScreen( + uiState: PuppyArticleUiState.Success, + navigateUp: () -> Unit, + onRatingClick: (Int) -> Unit, + imageLoader: ImageLoader, +) { + Surface( + color = HedvigTheme.colorScheme.backgroundPrimary, + modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing), + ) { + Column( + Modifier + .fillMaxSize(), + ) { + TopAppBarWithBack( + title = "", + onClick = navigateUp, + ) + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + ) { + 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, // todo + 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), + ) { + val headingColor = HedvigTheme.colorScheme.textPrimary + RichText( + style = RichTextStyle( + headingStyle = { _, currentStyle -> + currentStyle.copy( + color = headingColor, + ) + }, + stringStyle = RichTextStringStyle( + boldStyle = SpanStyle( + color = headingColor, + ), + ), + ), + ) { + Markdown( + content = uiState.story.content, + ) + } + } + Spacer(Modifier.height(48.dp)) + HedvigText(stringResource(R.string.PUPPY_GUIDE_RATING_QUESTION)) + Spacer(Modifier.height(16.dp)) + logcat { "Mariia: uiState.story.rating ${uiState.story.rating}" } + RatingSection( + onRatingClick = onRatingClick, + selectedRating = uiState.story.rating, + ) + Spacer(Modifier.height(16.dp)) + } + } + } +} + +@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) + Row( + horizontalArrangement = Arrangement.SpaceAround, + modifier = Modifier, + ) { + ratings.forEach { rating -> + val isSelectedRating = selectedRating == rating + logcat { "Mariia: isSelectedRating $isSelectedRating" } + HedvigCard( + modifier = Modifier.weight(1f), + onClick = { + 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(16.dp)) + HorizontalItemsWithMaximumSpaceTaken( + startSlot = { + HedvigText( + stringResource(R.string.PUPPY_GUIDE_RATING_NOT_HELPFUL), + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + ) + }, + endSlot = { + Row(horizontalArrangement = Arrangement.End) { + HedvigText( + stringResource(R.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(), + ) + } + } +} + +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/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleViewModel.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleViewModel.kt new file mode 100644 index 0000000000..1ddbdbfee6 --- /dev/null +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyArticleViewModel.kt @@ -0,0 +1,105 @@ +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.logger.logcat +import com.hedvig.android.molecule.android.MoleculeViewModel +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope + +internal class PuppyArticleViewModel( + getPuppyGuideUseCase: GetPuppyGuideUseCase, + setArticleRatingUseCase: SetArticleRatingUseCase, + storyName: String, +) : MoleculeViewModel( + presenter = PuppyArticlePresenter(getPuppyGuideUseCase, storyName, setArticleRatingUseCase), + initialState = PuppyArticleUiState.Loading, + ) + +private class PuppyArticlePresenter( + private val getPuppyGuideUseCase: GetPuppyGuideUseCase, + private val storyName: String, + private val setArticleRatingUseCase: SetArticleRatingUseCase, +) : 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) } + + CollectEvents { event -> + when (event) { + PuppyArticleEvent.Reload -> loadIteration++ + is PuppyArticleEvent.RatingClick -> { + rating = event.rating + } + } + } + + LaunchedEffect(loadIteration) { + getPuppyGuideUseCase.invoke().fold( + ifLeft = { + currentState = PuppyArticleUiState.Failure + }, + ifRight = { stories -> + val matchingStory = stories?.firstOrNull { it.name == storyName } + currentState = if (matchingStory == null) { + PuppyArticleUiState.Failure + } else { + logcat { "Mariia. Story rating is: ${matchingStory.rating} " } + 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 = { + // todo: snackbar? + }, + ifRight = { + logcat { "Mariia: rating set!" } + }, + ) + } + + 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 +} + +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/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideDestination.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideDestination.kt new file mode 100644 index 0000000000..8b557540c1 --- /dev/null +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideDestination.kt @@ -0,0 +1,383 @@ +package com.hedvig.android.feature.help.center.puppyguide + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +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.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +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 coil.ImageLoader +import coil.compose.AsyncImage +import com.hedvig.android.compose.ui.EmptyContentDescription +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.HedvigScaffold +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.HighlightLabel +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.TopAppBarWithBack +import com.hedvig.android.design.system.hedvig.rememberPreviewImageLoader +import com.hedvig.android.feature.help.center.data.PuppyGuideStory +import hedvig.resources.R +import kotlinx.coroutines.launch + +@Composable +internal fun PuppyGuideDestination( + viewModel: PuppyGuideViewModel, + onNavigateUp: () -> Unit, + imageLoader: ImageLoader, + onNavigateToArticle: (PuppyGuideStory) -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + PuppyGuideScreen( + uiState, + onNavigateToArticle = onNavigateToArticle, + onNavigateUp = onNavigateUp, + reload = { + viewModel.emit(PuppyGuideEvent.Reload) + }, + imageLoader = imageLoader, + ) +} + +@Composable +private fun PuppyGuideScreen( + uiState: PuppyGuideUiState, + onNavigateToArticle: (PuppyGuideStory) -> Unit, + onNavigateUp: () -> Unit, + reload: () -> Unit, + imageLoader: ImageLoader, +) { + when (uiState) { + PuppyGuideUiState.Failure -> HedvigScaffold( + navigateUp = onNavigateUp, + ) { + HedvigErrorSection( + onButtonClick = reload, + modifier = Modifier.weight(1f), + ) + } + + PuppyGuideUiState.Loading -> HedvigFullScreenCenterAlignedProgress() + is PuppyGuideUiState.Success -> PuppyGuideSuccessScreen( + uiState, + onNavigateUp = onNavigateUp, + onNavigateToArticle = onNavigateToArticle, + imageLoader = imageLoader, + ) + } +} + +@Composable +private fun PuppyGuideSuccessScreen( + uiState: PuppyGuideUiState.Success, + onNavigateToArticle: (PuppyGuideStory) -> Unit, + onNavigateUp: () -> Unit, + imageLoader: ImageLoader, +) { + val categories = uiState.stories.flatMap { it.categories }.toSet().toList() + var selectedCategory by remember { mutableStateOf(null) } + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + + LaunchedEffect(selectedCategory) { + selectedCategory?.let { cat -> + val index = categories.indexOf(cat) + if (index >= 0) { + // Negative offset to scroll less and avoid sticky header covering the title + scope.launch { + listState.animateScrollToItem( + index + 2, + scrollOffset = -200, // todo: wtf + ) + } + } + } + } + + Surface( + color = HedvigTheme.colorScheme.backgroundPrimary, + modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing), + ) { + Column( + Modifier.fillMaxSize(), + ) { + TopAppBarWithBack( + title = "", + onClick = onNavigateUp, + ) + + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + item { + Column { + Spacer(modifier = Modifier.height(8.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth(), + ) { + Image( + painter = painterResource(id = com.hedvig.android.feature.help.center.R.drawable.hundar_badar_pet), + contentDescription = null, + contentScale = ContentScale.Crop, + alignment = Alignment.Center, + modifier = Modifier + .height(300.dp) + .clip(HedvigTheme.shapes.cornerXLarge), + ) + } + Spacer(modifier = Modifier.height(16.dp)) + HedvigText(stringResource(R.string.PUPPY_GUIDE_TITLE)) + Spacer(modifier = Modifier.height(8.dp)) + HedvigText( + stringResource(R.string.PUPPY_GUIDE_INFO), + color = HedvigTheme.colorScheme.textSecondary, + ) + Spacer(modifier = Modifier.height(48.dp)) + } + } + + stickyHeader { + Surface( + color = HedvigTheme.colorScheme.backgroundPrimary, + modifier = Modifier.fillMaxWidth(), + ) { + Column { + GuideCategoriesRow( + categories, + onCategoryClick = { + selectedCategory = it + }, + ) + Spacer(modifier = Modifier.height(24.dp)) + } + } + } + + items(categories) { cat -> + CategoryWithArticlesSection( + cat, + stories = uiState.stories.filter { it.categories.contains(cat) }, + onNavigateToArticle = onNavigateToArticle, + imageLoader = imageLoader, + ) + } + } + } + } +} + +@Composable +private fun GuideCategoriesRow(categories: List, onCategoryClick: (String) -> Unit) { + Row(Modifier.horizontalScroll(rememberScrollState())) { + categories.forEach { + HedvigButton( + text = it, + enabled = true, + buttonSize = ButtonDefaults.ButtonSize.Medium, + buttonStyle = ButtonDefaults.ButtonStyle.Secondary, + onClick = { + onCategoryClick(it) + }, + ) + Spacer(Modifier.width(8.dp)) + } + } +} + +@Composable +private fun CategoryWithArticlesSection( + category: String, + stories: List, + onNavigateToArticle: (PuppyGuideStory) -> Unit, + imageLoader: ImageLoader, + modifier: Modifier = Modifier, +) { + Column(modifier) { + HedvigText( + category, + fontStyle = HedvigTheme.typography.headlineSmall.fontStyle, + fontSize = HedvigTheme.typography.headlineSmall.fontSize, + fontFamily = HedvigTheme.typography.headlineSmall.fontFamily, + ) + Spacer(Modifier.height(12.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(24.dp), + modifier = Modifier.horizontalScroll(rememberScrollState()), + ) { + val size = 148.dp + stories.forEach { 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, +) { + Column( + modifier + .width(size) + .clip(shape) + .clickable( + onClick = { + onNavigateToArticle(story) + }, + ), + ) { + Box( + contentAlignment = Alignment.TopEnd, + ) { + val fallbackPainter: Painter = ColorPainter(Color.Black.copy(alpha = 0.7f)) + AsyncImage( + model = story.image, + contentDescription = EmptyContentDescription, // todo + placeholder = fallbackPainter, + error = fallbackPainter, + fallback = fallbackPainter, + imageLoader = imageLoader, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(size) + .clip(shape), + ) + if (story.isRead || story.rating != null) { + HighlightLabel( + modifier = modifier.padding( + end = 12.dp, + top = 12.dp, + ), + labelText = stringResource(R.string.PUPPY_GUIDE_LABEL_READ), + size = HighlightLabelDefaults.HighLightSize.Small, + color = HighlightLabelDefaults.HighlightColor.Grey(HighlightLabelDefaults.HighlightShade.LIGHT), + ) + } + } + + Spacer(Modifier.height(8.dp)) + HedvigText( + story.title, + style = HedvigTheme.typography.label, + maxLines = 1, + overflow = TextOverflow.Ellipsis, // todo: not by a11y req + ) + HedvigText( + story.subtitle, + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondaryTranslucent, + ) + } +} + +@HedvigShortMultiScreenPreview +@Composable +private fun PuppyArticleScreenAnimations( + @PreviewParameter(PuppyGuideUiStatePreviewProvider::class) uiState: PuppyGuideUiState, +) { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + PuppyGuideScreen( + uiState, + {}, + {}, + reload = {}, + imageLoader = rememberPreviewImageLoader(), + ) + } + } +} + +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/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt new file mode 100644 index 0000000000..abfa3c0c58 --- /dev/null +++ b/app/feature/feature-help-center/src/main/kotlin/com/hedvig/android/feature/help/center/puppyguide/PuppyGuideViewModel.kt @@ -0,0 +1,68 @@ +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.android.MoleculeViewModel +import com.hedvig.android.molecule.public.MoleculePresenter +import com.hedvig.android.molecule.public.MoleculePresenterScope +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().fold( + ifLeft = { + currentState = PuppyGuideUiState.Failure + }, + ifRight = { stories -> + currentState = if (stories == null) { + PuppyGuideUiState.Failure + } else { + PuppyGuideUiState.Success(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/main/res/drawable/hundar_badar_pet.jpg b/app/feature/feature-help-center/src/main/res/drawable/hundar_badar_pet.jpg new file mode 100644 index 0000000000..c7e8f8bfc2 Binary files /dev/null and b/app/feature/feature-help-center/src/main/res/drawable/hundar_badar_pet.jpg differ