From 7234747cae96006c275cc2de961933c74ed95c4a Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Tue, 4 Aug 2026 13:42:45 +0200 Subject: [PATCH 01/23] title font --- .../com/hedvig/android/feature/home/home/ui/HomeDestination.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 1b17bbbf20..09eb6d97af 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -1336,8 +1336,7 @@ private fun WelcomeMessage(firstName: String, modifier: Modifier = Modifier) { // todo custom style since new DS does not have this specification // https://hedviginsurance.slack.com/archives/C03U9C6Q7TP/p1727365167917719 val titleStyle = HedvigTheme.typography.headlineMedium.copy( - fontFamily = HedvigTheme.typography.serif, - fontSize = 28.0.sp, + fontSize = 24.0.sp, lineBreak = LineBreak.Heading, textAlign = TextAlign.Center, ) From 778590bcf0b22642bba295edc018bd0b5233cf10 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Tue, 4 Aug 2026 13:53:18 +0200 Subject: [PATCH 02/23] blur only for light mode --- .../design/system/hedvig/ColorScheme.kt | 7 ++ .../feature/home/home/ui/HomeDestination.kt | 105 ++++++++---------- 2 files changed, 52 insertions(+), 60 deletions(-) diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ColorScheme.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ColorScheme.kt index 209a444730..a410e1e34b 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ColorScheme.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ColorScheme.kt @@ -13,6 +13,11 @@ import com.hedvig.android.design.system.hedvig.tokens.ColorSchemeKeyTokens @Immutable data class ColorScheme( + /** + * Whether this is the light color scheme. Read it for UI that cannot be expressed as a color token, + * such as a background image that only exists in one theme. + */ + val isLight: Boolean, val textPrimary: Color, val textNegative: Color, val textSecondary: Color, @@ -121,6 +126,7 @@ data class ColorScheme( ) internal val lightColorScheme: ColorScheme = ColorScheme( + isLight = true, textPrimary = ColorLightTokens.TextPrimary, textNegative = ColorLightTokens.TextNegative, textSecondary = ColorLightTokens.TextSecondary, @@ -229,6 +235,7 @@ internal val lightColorScheme: ColorScheme = ColorScheme( ) internal val darkColorScheme: ColorScheme = ColorScheme( + isLight = false, textPrimary = ColorDarkTokens.TextPrimary, textNegative = ColorDarkTokens.TextNegative, textSecondary = ColorDarkTokens.TextSecondary, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 09eb6d97af..0cce894bdb 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -429,37 +429,33 @@ private fun HomeScreen( if (currentState.firstVetAction != null) add(currentState.firstVetAction) if (currentState.chatAction != null) add(currentState.chatAction) } - OnHeroGradient { - actionsList.forEach { action -> - when (action) { - ChatAction -> { - // The FirstVet/cross-sell icons are colored images (theme-independent), but this one is a - // tinted glyph on a tonal surface, so it must use the light scheme on the light gradient. - ToolbarChatIcon( - onClick = onNavigateToInbox, - modifier = Modifier.notificationCircle(uiState.hasUnseenChatMessages), - ) - } + actionsList.forEach { action -> + when (action) { + ChatAction -> { + ToolbarChatIcon( + onClick = onNavigateToInbox, + modifier = Modifier.notificationCircle(uiState.hasUnseenChatMessages), + ) + } - is CrossSellsAction -> { - ToolbarCrossSellsIcon( - onClick = { - crossSellBottomSheetState.show( - action.crossSells, - ) - }, - modifier = Modifier.notificationCircle( - action.crossSellRecommendationNotification.hasUnreadRecommendation, - ), - ) - } + is CrossSellsAction -> { + ToolbarCrossSellsIcon( + onClick = { + crossSellBottomSheetState.show( + action.crossSells, + ) + }, + modifier = Modifier.notificationCircle( + action.crossSellRecommendationNotification.hasUnreadRecommendation, + ), + ) + } - is FirstVetAction -> { - val sections = action.sections - ToolbarFirstVetIcon( - onClick = { navigateToFirstVet(sections) }, - ) - } + is FirstVetAction -> { + val sections = action.sections + ToolbarFirstVetIcon( + onClick = { navigateToFirstVet(sections) }, + ) } } } @@ -595,14 +591,17 @@ private fun HomeScreenSuccess( .onConsumedWindowInsetsChanged { consumedWindowInsets.insets = it } .pullRefresh(pullRefreshState), ) { - // Full-screen blur gradient behind the whole home screen. Sections that need a solid surface draw - // their own background on top to "hide" it (the content cards already do; so do the pinned pills). - Image( - painter = painterResource(Res.drawable.blur_background), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.matchParentSize(), - ) + // Full-screen blur gradient behind the whole home screen, in the light theme only; the dark theme + // keeps the regular background. Sections that need a solid surface draw their own background on top + // to "hide" it (the content cards already do; so do the pinned pills). + if (HedvigTheme.colorScheme.isLight) { + Image( + painter = painterResource(Res.drawable.blur_background), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.matchParentSize(), + ) + } NotificationPermissionDialog(notificationPermissionState, openAppSettings) val horizontalInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal).exclude(consumedWindowInsets).asPaddingValues() @@ -770,9 +769,7 @@ private fun HomeScreenSuccess( } }, ) { - OnHeroGradient { - WelcomeSection(uiState.firstName) - } + WelcomeSection(uiState.firstName) } } } @@ -791,16 +788,14 @@ private fun HomeScreenSuccess( .pointerInput(Unit) { detectTapGestures {} }, ) { Spacer(Modifier.height(pinnedTopOffset)) - OnHeroGradient { - QuickActionCarouselSection( - isHelpCenterEnabled = uiState.isHelpCenterEnabled, - onMakeClaim = openClaimFlowSheet, - onHelpAndSupport = navigateToHelpCenter, - onContactUs = onNavigateToInbox, - horizontalInsets = horizontalInsets, - modifier = Modifier.padding(bottom = 8.dp), - ) - } + QuickActionCarouselSection( + isHelpCenterEnabled = uiState.isHelpCenterEnabled, + onMakeClaim = openClaimFlowSheet, + onHelpAndSupport = navigateToHelpCenter, + onContactUs = onNavigateToInbox, + horizontalInsets = horizontalInsets, + modifier = Modifier.padding(bottom = 8.dp), + ) HomeSheetDragHandle( Modifier .fillMaxWidth() @@ -970,16 +965,6 @@ private fun gapAfter(section: HomeSection): Dp = when (section) { else -> homeSectionGap } -/** - * The hero gradient ([Res.drawable.blur_background]) is the same light image in both light and dark - * themes, so content drawn on it (greeting, pills, the tonal chat icon) must always render with the - * light color scheme. Otherwise dark-theme text and tints turn near-invisible against the light gradient. - */ -@Composable -private fun OnHeroGradient(content: @Composable () -> Unit) { - HedvigTheme(darkTheme = false, content = content) -} - @Composable private fun WelcomeSection(firstName: String) { Column( From b567347a6280abe572f5903a0903c8b0cf2ef669 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Tue, 4 Aug 2026 16:37:25 +0200 Subject: [PATCH 03/23] change toolbar icons --- .../design/system/hedvig/icon/ChatNoCircle.kt | 85 ++++++++++++++ .../icon/colored/ColoredFirstVetNoCircle.kt | 101 +++++++++++++++++ .../feature/home/home/ui/ToolbarIcons.kt | 107 ++++++++---------- 3 files changed, 235 insertions(+), 58 deletions(-) create mode 100644 app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/ChatNoCircle.kt create mode 100644 app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/colored/ColoredFirstVetNoCircle.kt diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/ChatNoCircle.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/ChatNoCircle.kt new file mode 100644 index 0000000000..55d02a050f --- /dev/null +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/ChatNoCircle.kt @@ -0,0 +1,85 @@ +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.design.system.hedvig.HedvigTheme + +@Suppress("UnusedReceiverParameter") +val HedvigIcons.ChatNoCircle: ImageVector + get() { + if (_ChatNoCircle != null) { + return _ChatNoCircle!! + } + _ChatNoCircle = ImageVector.Builder( + name = "ChatNoCircle", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path( + fill = SolidColor(Color(0xFF121212)), + pathFillType = PathFillType.EvenOdd, + ) { + moveTo(5.118f, 5f) + curveTo(3.948f, 5f, 3f, 5.964f, 3f, 7.154f) + verticalLineTo(16.846f) + curveTo(3f, 18.036f, 3.948f, 19f, 5.118f, 19f) + horizontalLineTo(18.882f) + curveTo(20.052f, 19f, 21f, 18.036f, 21f, 16.846f) + verticalLineTo(7.154f) + curveTo(21f, 5.964f, 20.052f, 5f, 18.882f, 5f) + horizontalLineTo(5.118f) + close() + moveTo(12.583f, 13.224f) + curveTo(12.229f, 13.462f, 11.771f, 13.462f, 11.417f, 13.224f) + lineTo(5.458f, 9.228f) + curveTo(5.246f, 9.086f, 5.118f, 8.844f, 5.118f, 8.585f) + curveTo(5.118f, 7.971f, 5.789f, 7.604f, 6.292f, 7.942f) + lineTo(11.417f, 11.378f) + curveTo(11.771f, 11.616f, 12.229f, 11.616f, 12.583f, 11.378f) + lineTo(17.708f, 7.942f) + curveTo(18.211f, 7.604f, 18.882f, 7.971f, 18.882f, 8.585f) + curveTo(18.882f, 8.844f, 18.754f, 9.086f, 18.542f, 9.228f) + lineTo(12.583f, 13.224f) + close() + } + }.build() + + return _ChatNoCircle!! + } + +@Suppress("ObjectPropertyName") +private var _ChatNoCircle: ImageVector? = null + +@Preview +@Composable +private fun IconPreview() { + HedvigTheme { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + imageVector = HedvigIcons.ChatNoCircle, + contentDescription = com.hedvig.android.compose.ui.EmptyContentDescription, + modifier = Modifier + .width((40.0).dp) + .height((40.0).dp), + ) + } + } +} diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/colored/ColoredFirstVetNoCircle.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/colored/ColoredFirstVetNoCircle.kt new file mode 100644 index 0000000000..18f5a86c63 --- /dev/null +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/icon/colored/ColoredFirstVetNoCircle.kt @@ -0,0 +1,101 @@ +package com.hedvig.android.design.system.hedvig.icon.colored + +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.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.icon.HedvigIcons + +val HedvigIcons.ColoredFirstVetNoCircle: ImageVector + get() { + if (_FirstVetLogo != null) { + return _FirstVetLogo!! + } + _FirstVetLogo = ImageVector.Builder( + name = "FirstVetLogo", + defaultWidth = 24.dp, + defaultHeight = 19.dp, + viewportWidth = 24f, + viewportHeight = 19f, + ).apply { + path( + fill = SolidColor(Color(0xFF0062FF)), + pathFillType = PathFillType.EvenOdd, + ) { + moveTo(23.301f, 2.357f) + lineTo(19.411f, 4.422f) + curveTo(19.402f, 4.343f, 19.395f, 4.263f, 19.386f, 4.185f) + curveTo(19.146f, 2.127f, 17.488f, 0.518f, 15.434f, 0.29f) + curveTo(13.697f, 0.096f, 11.932f, 0f, 10.166f, 0f) + horizontalLineTo(9.532f) + curveTo(7.767f, 0f, 6.001f, 0.096f, 4.266f, 0.29f) + curveTo(2.212f, 0.518f, 0.553f, 2.127f, 0.311f, 4.182f) + curveTo(-0.104f, 7.715f, -0.104f, 11.283f, 0.311f, 14.815f) + curveTo(0.553f, 16.871f, 2.212f, 18.482f, 4.266f, 18.709f) + curveTo(6.001f, 18.902f, 7.767f, 19f, 9.532f, 19f) + horizontalLineTo(10.166f) + curveTo(11.93f, 19f, 13.697f, 18.904f, 15.432f, 18.709f) + curveTo(17.485f, 18.482f, 19.144f, 16.871f, 19.386f, 14.815f) + curveTo(19.395f, 14.735f, 19.402f, 14.657f, 19.411f, 14.576f) + lineTo(23.298f, 16.641f) + curveTo(23.617f, 16.808f, 24f, 16.58f, 24f, 16.221f) + verticalLineTo(2.775f) + curveTo(24f, 2.417f, 23.617f, 2.187f, 23.301f, 2.357f) + close() + moveTo(15.777f, 10.49f) + curveTo(15.64f, 11.265f, 14.898f, 11.783f, 14.12f, 11.645f) + lineTo(11.305f, 11.149f) + lineTo(10.561f, 15.358f) + lineTo(9.153f, 15.11f) + curveTo(8.375f, 14.974f, 7.855f, 14.234f, 7.994f, 13.459f) + lineTo(8.491f, 10.653f) + lineTo(4.268f, 9.911f) + lineTo(4.517f, 8.508f) + curveTo(4.653f, 7.733f, 5.395f, 7.214f, 6.173f, 7.353f) + lineTo(8.989f, 7.847f) + lineTo(9.733f, 3.637f) + lineTo(11.141f, 3.885f) + curveTo(11.919f, 4.022f, 12.439f, 4.761f, 12.3f, 5.536f) + lineTo(11.805f, 8.343f) + lineTo(16.028f, 9.084f) + lineTo(15.777f, 10.49f) + close() + } + }.build() + + return _FirstVetLogo!! + } + +@Suppress("ObjectPropertyName") +private var _FirstVetLogo: ImageVector? = null + +@Preview +@Composable +private fun IconPreview() { + HedvigTheme { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + imageVector = HedvigIcons.ColoredFirstVetNoCircle, + contentDescription = com.hedvig.android.compose.ui.EmptyContentDescription, + modifier = Modifier + .width((40.0).dp) + .height((40.0).dp), + ) + } + } +} diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/ToolbarIcons.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/ToolbarIcons.kt index 74d51b6c21..b9e4c375c3 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/ToolbarIcons.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/ToolbarIcons.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape @@ -27,12 +28,12 @@ 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.icon.Chat +import com.hedvig.android.design.system.hedvig.icon.Campaign +import com.hedvig.android.design.system.hedvig.icon.ChatNoCircle 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.colored.ColoredCampaign import com.hedvig.android.design.system.hedvig.icon.colored.ColoredChat -import com.hedvig.android.design.system.hedvig.icon.colored.ColoredFirstVet +import com.hedvig.android.design.system.hedvig.icon.colored.ColoredFirstVetNoCircle import hedvig.resources.DASHBOARD_OPEN_CHAT import hedvig.resources.HC_QUICK_ACTIONS_FIRSTVET_SUBTITLE import hedvig.resources.Res @@ -40,33 +41,49 @@ import hedvig.resources.home_tab_claim_button_text import hedvig.resources.insurance_tab_cross_sells_title import org.jetbrains.compose.resources.stringResource +// Diameter shared by every circular button in the home top app bar, and the glyph centered in it. +private val toolbarButtonSize = 48.dp +private val toolbarGlyphSize = 24.dp + +/** + * The circular surface every home top app bar button sits on: a [toolbarButtonSize] circle holding a + * centered [toolbarGlyphSize] glyph. + */ @Composable -fun ToolbarChatIcon(onClick: () -> Unit, modifier: Modifier = Modifier) { - Icon( - imageVector = HedvigIcons.Chat, - contentDescription = stringResource(Res.string.DASHBOARD_OPEN_CHAT), - tint = HedvigTheme.colorScheme.signalGreyElement, +private fun ToolbarIconButton(onClick: () -> Unit, modifier: Modifier = Modifier, glyph: @Composable () -> Unit) { + Box( + contentAlignment = Alignment.Center, modifier = modifier - .size(40.dp) + .size(toolbarButtonSize) .shadow(4.dp, CircleShape) .clip(CircleShape) - .background(HedvigTheme.colorScheme.surfacePrimaryTransparent) + .background(HedvigTheme.colorScheme.surfacePrimary) .clickable(role = Role.Button, onClick = onClick), + content = { glyph() }, ) } +@Composable +fun ToolbarChatIcon(onClick: () -> Unit, modifier: Modifier = Modifier) { + ToolbarIconButton(onClick = onClick, modifier = modifier) { + Icon( + imageVector = HedvigIcons.ChatNoCircle, + contentDescription = stringResource(Res.string.DASHBOARD_OPEN_CHAT), + tint = HedvigTheme.colorScheme.fillPrimary, + modifier = Modifier.size(toolbarGlyphSize), + ) + } +} + @Composable fun ToolbarFirstVetIcon(onClick: () -> Unit, modifier: Modifier = Modifier) { - Image( - imageVector = HedvigIcons.ColoredFirstVet, - contentDescription = stringResource(Res.string.HC_QUICK_ACTIONS_FIRSTVET_SUBTITLE), - modifier = modifier - .size(40.dp) - .shadow(4.dp, CircleShape) - .clip(CircleShape) - .background(HedvigTheme.colorScheme.surfacePrimary) - .clickable(role = Role.Button, onClick = onClick), - ) + ToolbarIconButton(onClick = onClick, modifier = modifier) { + Image( + imageVector = HedvigIcons.ColoredFirstVetNoCircle, + contentDescription = stringResource(Res.string.HC_QUICK_ACTIONS_FIRSTVET_SUBTITLE), + modifier = Modifier.size(toolbarGlyphSize), + ) + } } @Composable @@ -76,41 +93,18 @@ fun ToolbarCrossSellsIcon(onClick: () -> Unit, modifier: Modifier = Modifier) { targetValue = if (isRotated) 360f else 0f, animationSpec = tween(1500, 50), ) - Image( - imageVector = HedvigIcons.ColoredCampaign, - contentDescription = stringResource(Res.string.insurance_tab_cross_sells_title), - modifier = modifier - // Spin the whole circular icon as one unit (outermost transform), so the rotation stays within - // the circular clip and shadow. - .graphicsLayer { rotationZ = fullRotation } - .size(40.dp) - .shadow(4.dp, CircleShape) - .clip(CircleShape) - .background(HedvigTheme.colorScheme.surfacePrimary) - .clickable(role = Role.Button, onClick = onClick), - ) -} - -@Composable -fun ToolbarClaimChatIcon(onClick: () -> Unit, modifier: Modifier = Modifier, isDev: Boolean = false) { - Box { + ToolbarIconButton( + onClick = onClick, + // Spin the whole circular icon as one unit (outermost transform), so the rotation stays within + // the circular clip and shadow. + modifier = modifier.graphicsLayer { rotationZ = fullRotation }, + ) { Icon( - imageVector = HedvigIcons.ColoredChat, - contentDescription = stringResource(Res.string.home_tab_claim_button_text), - tint = Color.Unspecified, - modifier = modifier - .size(40.dp) - .shadow(4.dp, CircleShape) - .clip(CircleShape) - .clickable(role = Role.Button, onClick = onClick), + imageVector = HedvigIcons.Campaign, + contentDescription = stringResource(Res.string.insurance_tab_cross_sells_title), + tint = HedvigTheme.colorScheme.signalGreenElement, + modifier = Modifier.size(toolbarGlyphSize), ) - if (isDev) { - HedvigText( - "dev", - style = HedvigTheme.typography.label, - modifier = Modifier.align(Alignment.Center), - ) - } } } @@ -120,14 +114,11 @@ private fun PreviewToolbarChatIcon() { HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { Row { - ToolbarClaimChatIcon({}, isDev = true) - Spacer(modifier = Modifier.width(8.dp)) - ToolbarClaimChatIcon({}) + ToolbarFirstVetIcon(onClick = {}) Spacer(modifier = Modifier.width(8.dp)) ToolbarCrossSellsIcon({}) Spacer(modifier = Modifier.width(8.dp)) - ToolbarFirstVetIcon(onClick = {}) - Spacer(modifier = Modifier.width(8.dp)) + ToolbarChatIcon({}) } } From 3d186a3dcd98178cd7b51926844a6ca02d484f9d Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Tue, 4 Aug 2026 19:15:33 +0200 Subject: [PATCH 04/23] sheet border --- .../feature/home/home/ui/HomeDestination.kt | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 0cce894bdb..45302f0438 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement @@ -54,7 +55,12 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.clipRect +import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource @@ -689,10 +695,14 @@ private fun HomeScreenSuccess( } } } + // The sheet's surface is painted by three things that have to agree: this backdrop, the pinned lid, + // and every scrolling section's own background. Only the parts the sections don't cover are actually + // this backdrop, so changing one of the three alone leaves the rest at the old color. + val sheetColor = HedvigTheme.colorScheme.backgroundPrimary + val sheetShape = HedvigTheme.shapes.cornerXLargeTop // Opaque sheet backdrop from the pinned lid down to the bottom, so the sheet fills the screen even // when the content is short (e.g. landscape) — no blur gap below the last section. Drawn behind the // LazyColumn (on top of the blur); the hero + pills above the lid stay transparent on the gradient. - val sheetColor = HedvigTheme.colorScheme.backgroundPrimary val contentMaxWidth = 600.dp Box( Modifier @@ -799,10 +809,8 @@ private fun HomeScreenSuccess( HomeSheetDragHandle( Modifier .fillMaxWidth() - .background( - color = HedvigTheme.colorScheme.backgroundPrimary, - shape = HedvigTheme.shapes.cornerXLargeTop, - ), + .background(color = sheetColor, shape = sheetShape) + .topEdgeBorder(sheetShape, HedvigTheme.colorScheme.borderPrimary), ) } } @@ -824,7 +832,7 @@ private fun HomeScreenSuccess( val clipTop = (stickyHeaderBottomPx - itemTopPx).coerceIn(0f, size.height) clipRect(top = clipTop) { this@drawWithContent.drawContent() } } - .background(HedvigTheme.colorScheme.backgroundPrimary), + .background(sheetColor), ) { // The first scrolling section keeps a leading gap below the pinned lid; every section then carries // its gap to the NEXT one as trailing room, giving drop-shadows space within the section. @@ -904,6 +912,23 @@ private fun HomeScreenSuccess( } } +/** + * Strokes only the top of [shape]: its rounded corners and the edge between them. The stroke runs down + * the sides for the height of the element it is applied to and stops there, and the bottom edge is + * clipped away entirely, so the surface below continues borderless. + */ +private fun Modifier.topEdgeBorder(shape: Shape, color: Color, width: Dp = 1.dp): Modifier = drawWithContent { + drawContent() + val stroke = width.toPx() + // Outline sized and offset so the whole stroke lands inside the element rather than straddling its edge. + val outline = shape.createOutline(Size(size.width - stroke, size.height), layoutDirection, this) + clipRect(bottom = size.height - stroke) { + translate(left = stroke / 2f, top = stroke / 2f) { + drawOutline(outline, color = color, style = Stroke(stroke)) + } + } +} + @Composable private fun HomeSheetDragHandle(modifier: Modifier = Modifier) { Box( From 9fda8c48fd876234e87b4ad7ebc4eaa61681110e Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 5 Aug 2026 09:37:44 +0200 Subject: [PATCH 05/23] member reminders border --- .../com/hedvig/android/design/system/hedvig/HedvigCard.kt | 1 + .../com/hedvig/android/feature/home/home/ui/HomeDestination.kt | 2 +- .../android/memberreminders/ui/MemberReminderToDoList.kt | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigCard.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigCard.kt index fb95a35cd9..e81f7003ad 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigCard.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigCard.kt @@ -80,6 +80,7 @@ fun HedvigCard( shape = shape, color = color, modifier = modifier, + border = borderColor, ) { content() } diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 45302f0438..440a80d381 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -804,7 +804,7 @@ private fun HomeScreenSuccess( onHelpAndSupport = navigateToHelpCenter, onContactUs = onNavigateToInbox, horizontalInsets = horizontalInsets, - modifier = Modifier.padding(bottom = 8.dp), + modifier = Modifier.padding(bottom = 16.dp), ) HomeSheetDragHandle( Modifier diff --git a/app/member-reminders/member-reminders-ui/src/main/kotlin/com/hedvig/android/memberreminders/ui/MemberReminderToDoList.kt b/app/member-reminders/member-reminders-ui/src/main/kotlin/com/hedvig/android/memberreminders/ui/MemberReminderToDoList.kt index a82fb447ab..1ba0d11ffd 100644 --- a/app/member-reminders/member-reminders-ui/src/main/kotlin/com/hedvig/android/memberreminders/ui/MemberReminderToDoList.kt +++ b/app/member-reminders/member-reminders-ui/src/main/kotlin/com/hedvig/android/memberreminders/ui/MemberReminderToDoList.kt @@ -74,6 +74,9 @@ fun MemberReminderToDoList( if (rows.isEmpty()) return HedvigCard( color = HedvigTheme.colorScheme.fillNegative, + // fillNegative matches the surface this sits on in both themes, and the drop shadow that would + // otherwise separate them is light-only, so the outline is what gives the card an edge in dark mode. + borderColor = HedvigTheme.colorScheme.borderPrimary, modifier = modifier .fillMaxWidth() .hedvigDropShadow(HedvigTheme.shapes.cornerXLarge), From 84cd7690b5dc08153c979989f2a5e532ff35df11 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 5 Aug 2026 09:44:16 +0200 Subject: [PATCH 06/23] spacers --- .../feature/home/home/ui/HomeDestination.kt | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 440a80d381..d623808277 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -834,11 +834,8 @@ private fun HomeScreenSuccess( } .background(sheetColor), ) { - // The first scrolling section keeps a leading gap below the pinned lid; every section then carries - // its gap to the NEXT one as trailing room, giving drop-shadows space within the section. - if (index == 0) { - Spacer(Modifier.height(gapAfter(HomeSection.QuickActionCarousel))) - } + // Each section carries its gap to the NEXT one as trailing room, giving drop-shadows space + // within the section. The first one starts flush under the pinned lid. when (section) { HomeSection.Welcome, HomeSection.QuickActionCarousel -> Unit @@ -904,7 +901,7 @@ private fun HomeScreenSuccess( ) } if (next != null) { - Spacer(Modifier.height(gapAfter(section))) + Spacer(Modifier.height(homeSectionGap)) } } } @@ -976,20 +973,10 @@ private val homeSectionOrder: List = listOf( HomeSection.Addons, ) -// Gap below the pinned lid before the first scrolling section. -private val homeSectionLeadInGap = 24.dp - -// Uniform gap between two consecutive scrolling sections. +// Gap between two consecutive scrolling sections, carried as trailing room below a section's content so +// a card's drop-shadow renders inside the section's own bounds. private val homeSectionGap = 40.dp -// The gap that follows [section], carried as trailing room below its content so a card's drop-shadow -// renders inside the section's own bounds. The pinned lid (QuickActionCarousel) gets the smaller -// lead-in gap; every scrolling section is followed by the uniform section gap. -private fun gapAfter(section: HomeSection): Dp = when (section) { - HomeSection.QuickActionCarousel -> homeSectionLeadInGap - else -> homeSectionGap -} - @Composable private fun WelcomeSection(firstName: String) { Column( From 31d4ddf437a06f8c7c5a186f3ad74fce88e583b8 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 5 Aug 2026 11:14:06 +0200 Subject: [PATCH 07/23] liquid button v1 --- .../android/design/system/hedvig/Button.kt | 186 +++++++++++++++++- .../design/system/hedvig/ColorScheme.kt | 72 +++++++ .../android/design/system/hedvig/Shapes.kt | 2 + .../system/hedvig/tokens/ButtonTokens.kt | 34 ++++ .../system/hedvig/tokens/ColorDarkTokens.kt | 25 +++ .../system/hedvig/tokens/ColorLightTokens.kt | 25 +++ .../hedvig/tokens/ColorSchemeKeyTokens.kt | 18 ++ .../system/hedvig/tokens/PaletteTokens.kt | 8 + .../system/hedvig/tokens/ShapeKeyTokens.kt | 1 + .../feature/home/home/ui/HomeDestination.kt | 40 ++-- 10 files changed, 385 insertions(+), 26 deletions(-) diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt index 8336f8a8c3..8555ab4980 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt @@ -7,10 +7,12 @@ import androidx.compose.animation.core.updateTransition import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope @@ -24,14 +26,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.dropShadow +import androidx.compose.ui.draw.innerShadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.graphics.shadow.Shadow import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import com.hedvig.android.compose.ui.LayoutWithoutPlacement import com.hedvig.android.compose.ui.withoutPlacement @@ -43,6 +49,9 @@ import com.hedvig.android.design.system.hedvig.tokens.MiniSizeButtonTokens import com.hedvig.android.design.system.hedvig.tokens.PrimaryAltStyleButtonTokens import com.hedvig.android.design.system.hedvig.tokens.PrimaryStyleButtonTokens import com.hedvig.android.design.system.hedvig.tokens.RedStyleButtonTokens +import com.hedvig.android.design.system.hedvig.tokens.RoundedLargeSizeButtonTokens +import com.hedvig.android.design.system.hedvig.tokens.RoundedLiquidGlassStyleButtonTokens +import com.hedvig.android.design.system.hedvig.tokens.RoundedPrimaryStyleButtonTokens import com.hedvig.android.design.system.hedvig.tokens.SecondaryAltStyleButtonTokens import com.hedvig.android.design.system.hedvig.tokens.SecondaryStyleButtonTokens import com.hedvig.android.design.system.hedvig.tokens.SmallSizeButtonTokens @@ -119,7 +128,9 @@ fun HedvigButton( ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } - val buttonColors = buttonStyle.style.buttonColors + val style = buttonStyle.style + val size = buttonSize.sizeIn(style) + val buttonColors = style.buttonColors val containerColor = buttonColors.containerColor(enabled) val contentColor = buttonColors.contentColor(enabled) val isHovered by interactionSource.collectIsHoveredAsState() @@ -131,21 +142,35 @@ fun HedvigButton( containerColor }, ) + + @Suppress("NAME_SHADOWING") + val shape = shape ?: size.shape + val glass = style.glassContainer.takeIf { enabled } Surface( onClick = onClick, - modifier = modifier, + modifier = if (glass == null) { + modifier + } else { + modifier + .dropShadow(shape, glass.dropShadow) + .background(color, shape) + .innerShadow(shape, glass.rimShade) + .innerShadow(shape, glass.rimSheen) + }, onClickLabel = onClickLabel, role = Role.Button, enabled = enabled, - shape = shape ?: buttonSize.size.shape, + shape = shape, border = border, - color = color, + // The rim shadows have to sit on top of the container fill, which is only reachable from outside + // this Surface, so the fill moves into the modifier above and the Surface itself stays see-through. + color = if (glass == null) color else Color.Transparent, contentColor = contentColor, interactionSource = interactionSource, ) { - ProvideTextStyle(buttonSize.size.textStyle) { + ProvideTextStyle(size.textStyle) { Row( - modifier = Modifier.padding(buttonSize.size.contentPadding), + modifier = Modifier.padding(size.contentPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content, @@ -246,6 +271,31 @@ fun HedvigButtonGhostWithBorder( ) } +@HedvigPreview +@Composable +private fun PreviewRoundedButtons() { + HedvigTheme { + // A tinted backdrop, so that the translucent liquid glass container is distinguishable from it. + Surface(color = HedvigTheme.colorScheme.surfaceSecondary) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(16.dp), + ) { + for (buttonStyle in listOf( + ButtonDefaults.ButtonStyle.RoundedPrimary, + ButtonDefaults.ButtonStyle.RoundedLiquidGlass, + )) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + HedvigButton("Make a claim", {}, enabled = true, buttonStyle = buttonStyle) + HedvigButton("Disabled", {}, enabled = false, buttonStyle = buttonStyle) + HedvigButton("Loading", {}, enabled = true, buttonStyle = buttonStyle, isLoading = true) + } + } + } + } + } +} + object ButtonDefaults { internal val buttonStyle: ButtonStyle = ButtonStyle.Primary internal val buttonSize: ButtonSize = ButtonSize.Large @@ -257,6 +307,12 @@ object ButtonDefaults { SecondaryAlt, Ghost, Red, + + /** A filled pill with the sheen and drop shadow of the iOS glass material. */ + RoundedPrimary, + + /** A translucent pill of the iOS glass material, letting the backdrop show through. */ + RoundedLiquidGlass, } enum class ButtonSize { @@ -275,6 +331,8 @@ private val ButtonDefaults.ButtonStyle.style: Style ButtonDefaults.ButtonStyle.SecondaryAlt -> Style.SecondaryAlt ButtonDefaults.ButtonStyle.Ghost -> Style.Ghost ButtonDefaults.ButtonStyle.Red -> Style.Red + ButtonDefaults.ButtonStyle.RoundedPrimary -> Style.RoundedPrimary + ButtonDefaults.ButtonStyle.RoundedLiquidGlass -> Style.RoundedLiquidGlass } private val ButtonSize.size: Size @@ -285,6 +343,48 @@ private val ButtonSize.size: Size ButtonSize.Mini -> Size.Mini } +/** + * The metrics this size takes on within [style]. The rounded styles get their own pill metrics at + * [ButtonSize.Large]; at every smaller size they fall back to the standard button metrics. + */ +@Composable +private fun ButtonSize.sizeIn(style: Style): Size = when { + style.glassContainer != null && this == ButtonSize.Large -> Size.LargeRounded + else -> size +} + +/** + * The container treatment of the iOS glass material: a [dropShadow] behind the container, and two + * inner shadows over its fill that shade the top-left rim and light up the bottom-right one, which is + * how the material reads as lit from below and to the right. + */ +@Immutable +private data class GlassContainer(val dropShadow: Shadow, val rimShade: Shadow, val rimSheen: Shadow) + +private val glassDropShadow = Shadow( + radius = 40.dp, + color = Color.Black, + offset = DpOffset(0.dp, 8.dp), + alpha = 0.12f, +) + +// TODO: the rim shadows below stand in for a Figma glass effect that cannot be exported, so their +// values are read off the Figma render rather than given by design. Revisit once design catches up. + +/** The rim as the glass material normally reads: a faint shade and a pronounced sheen. */ +private val regularGlassContainer = GlassContainer( + dropShadow = glassDropShadow, + rimShade = Shadow(radius = 10.dp, color = Color.Black, offset = DpOffset(4.dp, 4.dp), alpha = 0.06f), + rimSheen = Shadow(radius = 10.dp, color = Color.White, offset = DpOffset((-4).dp, (-4).dp), alpha = 0.80f), +) + +/** The rim weighted for an opaque near-black fill, which would read as a grey smudge under [regularGlassContainer]. */ +private val opaqueDarkGlassContainer = GlassContainer( + dropShadow = glassDropShadow, + rimShade = Shadow(radius = 10.dp, color = Color.Black, offset = DpOffset(4.dp, 4.dp), alpha = 0.40f), + rimSheen = Shadow(radius = 10.dp, color = Color.White, offset = DpOffset((-4).dp, (-4).dp), alpha = 0.04f), +) + @Immutable private data class ButtonColors( val containerColor: Color, @@ -343,6 +443,25 @@ private sealed interface Size { get() = LargeSizeButtonTokens.ContainerShape.value } + object LargeRounded : Size { + override val contentPadding: PaddingValues = PaddingValues( + top = RoundedLargeSizeButtonTokens.TopPadding, + bottom = RoundedLargeSizeButtonTokens.BottomPadding, + start = RoundedLargeSizeButtonTokens.HorizontalPadding, + end = RoundedLargeSizeButtonTokens.HorizontalPadding, + ) + + override val textStyle: TextStyle + @Composable + @ReadOnlyComposable + get() = RoundedLargeSizeButtonTokens.LabelTextFont.value + + override val shape: Shape + @Composable + @ReadOnlyComposable + get() = RoundedLargeSizeButtonTokens.ContainerShape.value + } + object Medium : Size { override val contentPadding: PaddingValues = PaddingValues( top = MediumSizeButtonTokens.TopPadding, @@ -405,6 +524,11 @@ private sealed interface Style { @get:Composable val buttonColors: ButtonColors + /** Non-null on the styles that render the iOS glass material. */ + val glassContainer: GlassContainer? + @Composable + get() = null + data object Primary : Style { override val buttonColors: ButtonColors @Composable @@ -505,6 +629,56 @@ private sealed interface Style { } } + data object RoundedPrimary : Style { + // The fill inverts between themes: near-black on light, opaque white on dark. + override val glassContainer: GlassContainer + @Composable + get() = if (HedvigTheme.colorScheme.isLight) opaqueDarkGlassContainer else regularGlassContainer + + override val buttonColors: ButtonColors + @Composable + get() = with(HedvigTheme.colorScheme) { + remember(this) { + ButtonColors( + containerColor = fromToken(RoundedPrimaryStyleButtonTokens.ContainerColor), + contentColor = fromToken(RoundedPrimaryStyleButtonTokens.ContentColor), + disabledContainerColor = fromToken(RoundedPrimaryStyleButtonTokens.DisabledContainerColor), + disabledContentColor = fromToken(RoundedPrimaryStyleButtonTokens.DisabledContentColor), + hoverContainerColor = fromToken(RoundedPrimaryStyleButtonTokens.HoverContainerColor), + hoverContentColor = fromToken(RoundedPrimaryStyleButtonTokens.HoverContentColor), + activeLoadingIndicatorColor = fromToken(RoundedPrimaryStyleButtonTokens.ActiveLoadingIndicatorColor), + inactiveLoadingIndicatorColor = fromToken(RoundedPrimaryStyleButtonTokens.InactiveLoadingIndicatorColor), + redTextColor = fromToken(RoundedPrimaryStyleButtonTokens.RedContentColor), + ) + } + } + } + + data object RoundedLiquidGlass : Style { + override val glassContainer: GlassContainer + @Composable + get() = regularGlassContainer + + override val buttonColors: ButtonColors + @Composable + get() = with(HedvigTheme.colorScheme) { + remember(this) { + ButtonColors( + containerColor = fromToken(RoundedLiquidGlassStyleButtonTokens.ContainerColor), + contentColor = fromToken(RoundedLiquidGlassStyleButtonTokens.ContentColor), + disabledContainerColor = fromToken(RoundedLiquidGlassStyleButtonTokens.DisabledContainerColor), + disabledContentColor = fromToken(RoundedLiquidGlassStyleButtonTokens.DisabledContentColor), + hoverContainerColor = fromToken(RoundedLiquidGlassStyleButtonTokens.HoverContainerColor), + hoverContentColor = fromToken(RoundedLiquidGlassStyleButtonTokens.HoverContentColor), + activeLoadingIndicatorColor = fromToken(RoundedLiquidGlassStyleButtonTokens.ActiveLoadingIndicatorColor), + inactiveLoadingIndicatorColor = + fromToken(RoundedLiquidGlassStyleButtonTokens.InactiveLoadingIndicatorColor), + redTextColor = fromToken(RoundedLiquidGlassStyleButtonTokens.RedContentColor), + ) + } + } + } + data object Red : Style { override val buttonColors: ButtonColors @Composable diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ColorScheme.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ColorScheme.kt index a410e1e34b..fadf70c8a7 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ColorScheme.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ColorScheme.kt @@ -51,6 +51,24 @@ data class ColorScheme( val buttonGhostResting: Color, val buttonGhostHover: Color, val buttonGhostDisabled: Color, + val buttonRoundedPrimaryResting: Color, + val buttonRoundedPrimaryHover: Color, + val buttonRoundedPrimaryDisabled: Color, + val buttonRoundedPrimaryContent: Color, + val buttonRoundedPrimaryHoverContent: Color, + val buttonRoundedPrimaryDisabledContent: Color, + val buttonRoundedPrimaryLoadingActive: Color, + val buttonRoundedPrimaryLoadingInactive: Color, + val buttonRoundedPrimaryRedContent: Color, + val buttonRoundedLiquidGlassResting: Color, + val buttonRoundedLiquidGlassHover: Color, + val buttonRoundedLiquidGlassDisabled: Color, + val buttonRoundedLiquidGlassContent: Color, + val buttonRoundedLiquidGlassHoverContent: Color, + val buttonRoundedLiquidGlassDisabledContent: Color, + val buttonRoundedLiquidGlassLoadingActive: Color, + val buttonRoundedLiquidGlassLoadingInactive: Color, + val buttonRoundedLiquidGlassRedContent: Color, val fillPrimary: Color, val fillSecondary: Color, val fillTertiary: Color, @@ -160,6 +178,24 @@ internal val lightColorScheme: ColorScheme = ColorScheme( buttonGhostResting = ColorLightTokens.ButtonGhostResting, buttonGhostHover = ColorLightTokens.ButtonGhostHover, buttonGhostDisabled = ColorLightTokens.ButtonGhostDisabled, + buttonRoundedPrimaryResting = ColorLightTokens.ButtonRoundedPrimaryResting, + buttonRoundedPrimaryHover = ColorLightTokens.ButtonRoundedPrimaryHover, + buttonRoundedPrimaryDisabled = ColorLightTokens.ButtonRoundedPrimaryDisabled, + buttonRoundedPrimaryContent = ColorLightTokens.ButtonRoundedPrimaryContent, + buttonRoundedPrimaryHoverContent = ColorLightTokens.ButtonRoundedPrimaryHoverContent, + buttonRoundedPrimaryDisabledContent = ColorLightTokens.ButtonRoundedPrimaryDisabledContent, + buttonRoundedPrimaryLoadingActive = ColorLightTokens.ButtonRoundedPrimaryLoadingActive, + buttonRoundedPrimaryLoadingInactive = ColorLightTokens.ButtonRoundedPrimaryLoadingInactive, + buttonRoundedPrimaryRedContent = ColorLightTokens.ButtonRoundedPrimaryRedContent, + buttonRoundedLiquidGlassResting = ColorLightTokens.ButtonRoundedLiquidGlassResting, + buttonRoundedLiquidGlassHover = ColorLightTokens.ButtonRoundedLiquidGlassHover, + buttonRoundedLiquidGlassDisabled = ColorLightTokens.ButtonRoundedLiquidGlassDisabled, + buttonRoundedLiquidGlassContent = ColorLightTokens.ButtonRoundedLiquidGlassContent, + buttonRoundedLiquidGlassHoverContent = ColorLightTokens.ButtonRoundedLiquidGlassHoverContent, + buttonRoundedLiquidGlassDisabledContent = ColorLightTokens.ButtonRoundedLiquidGlassDisabledContent, + buttonRoundedLiquidGlassLoadingActive = ColorLightTokens.ButtonRoundedLiquidGlassLoadingActive, + buttonRoundedLiquidGlassLoadingInactive = ColorLightTokens.ButtonRoundedLiquidGlassLoadingInactive, + buttonRoundedLiquidGlassRedContent = ColorLightTokens.ButtonRoundedLiquidGlassRedContent, fillPrimary = ColorLightTokens.FillPrimary, fillSecondary = ColorLightTokens.FillSecondary, fillTertiary = ColorLightTokens.FillTertiary, @@ -269,6 +305,24 @@ internal val darkColorScheme: ColorScheme = ColorScheme( buttonGhostResting = ColorDarkTokens.ButtonGhostResting, buttonGhostHover = ColorDarkTokens.ButtonGhostHover, buttonGhostDisabled = ColorDarkTokens.ButtonGhostDisabled, + buttonRoundedPrimaryResting = ColorDarkTokens.ButtonRoundedPrimaryResting, + buttonRoundedPrimaryHover = ColorDarkTokens.ButtonRoundedPrimaryHover, + buttonRoundedPrimaryDisabled = ColorDarkTokens.ButtonRoundedPrimaryDisabled, + buttonRoundedPrimaryContent = ColorDarkTokens.ButtonRoundedPrimaryContent, + buttonRoundedPrimaryHoverContent = ColorDarkTokens.ButtonRoundedPrimaryHoverContent, + buttonRoundedPrimaryDisabledContent = ColorDarkTokens.ButtonRoundedPrimaryDisabledContent, + buttonRoundedPrimaryLoadingActive = ColorDarkTokens.ButtonRoundedPrimaryLoadingActive, + buttonRoundedPrimaryLoadingInactive = ColorDarkTokens.ButtonRoundedPrimaryLoadingInactive, + buttonRoundedPrimaryRedContent = ColorDarkTokens.ButtonRoundedPrimaryRedContent, + buttonRoundedLiquidGlassResting = ColorDarkTokens.ButtonRoundedLiquidGlassResting, + buttonRoundedLiquidGlassHover = ColorDarkTokens.ButtonRoundedLiquidGlassHover, + buttonRoundedLiquidGlassDisabled = ColorDarkTokens.ButtonRoundedLiquidGlassDisabled, + buttonRoundedLiquidGlassContent = ColorDarkTokens.ButtonRoundedLiquidGlassContent, + buttonRoundedLiquidGlassHoverContent = ColorDarkTokens.ButtonRoundedLiquidGlassHoverContent, + buttonRoundedLiquidGlassDisabledContent = ColorDarkTokens.ButtonRoundedLiquidGlassDisabledContent, + buttonRoundedLiquidGlassLoadingActive = ColorDarkTokens.ButtonRoundedLiquidGlassLoadingActive, + buttonRoundedLiquidGlassLoadingInactive = ColorDarkTokens.ButtonRoundedLiquidGlassLoadingInactive, + buttonRoundedLiquidGlassRedContent = ColorDarkTokens.ButtonRoundedLiquidGlassRedContent, fillPrimary = ColorDarkTokens.FillPrimary, fillSecondary = ColorDarkTokens.FillSecondary, fillTertiary = ColorDarkTokens.FillTertiary, @@ -403,6 +457,24 @@ internal fun ColorScheme.fromToken(value: ColorSchemeKeyTokens): Color { ColorSchemeKeyTokens.ButtonGhostResting -> buttonGhostResting ColorSchemeKeyTokens.ButtonGhostHover -> buttonGhostHover ColorSchemeKeyTokens.ButtonGhostDisabled -> buttonGhostDisabled + ColorSchemeKeyTokens.ButtonRoundedPrimaryResting -> buttonRoundedPrimaryResting + ColorSchemeKeyTokens.ButtonRoundedPrimaryHover -> buttonRoundedPrimaryHover + ColorSchemeKeyTokens.ButtonRoundedPrimaryDisabled -> buttonRoundedPrimaryDisabled + ColorSchemeKeyTokens.ButtonRoundedPrimaryContent -> buttonRoundedPrimaryContent + ColorSchemeKeyTokens.ButtonRoundedPrimaryHoverContent -> buttonRoundedPrimaryHoverContent + ColorSchemeKeyTokens.ButtonRoundedPrimaryDisabledContent -> buttonRoundedPrimaryDisabledContent + ColorSchemeKeyTokens.ButtonRoundedPrimaryLoadingActive -> buttonRoundedPrimaryLoadingActive + ColorSchemeKeyTokens.ButtonRoundedPrimaryLoadingInactive -> buttonRoundedPrimaryLoadingInactive + ColorSchemeKeyTokens.ButtonRoundedPrimaryRedContent -> buttonRoundedPrimaryRedContent + ColorSchemeKeyTokens.ButtonRoundedLiquidGlassResting -> buttonRoundedLiquidGlassResting + ColorSchemeKeyTokens.ButtonRoundedLiquidGlassHover -> buttonRoundedLiquidGlassHover + ColorSchemeKeyTokens.ButtonRoundedLiquidGlassDisabled -> buttonRoundedLiquidGlassDisabled + ColorSchemeKeyTokens.ButtonRoundedLiquidGlassContent -> buttonRoundedLiquidGlassContent + ColorSchemeKeyTokens.ButtonRoundedLiquidGlassHoverContent -> buttonRoundedLiquidGlassHoverContent + ColorSchemeKeyTokens.ButtonRoundedLiquidGlassDisabledContent -> buttonRoundedLiquidGlassDisabledContent + ColorSchemeKeyTokens.ButtonRoundedLiquidGlassLoadingActive -> buttonRoundedLiquidGlassLoadingActive + ColorSchemeKeyTokens.ButtonRoundedLiquidGlassLoadingInactive -> buttonRoundedLiquidGlassLoadingInactive + ColorSchemeKeyTokens.ButtonRoundedLiquidGlassRedContent -> buttonRoundedLiquidGlassRedContent ColorSchemeKeyTokens.FillPrimary -> fillPrimary ColorSchemeKeyTokens.FillSecondary -> fillSecondary ColorSchemeKeyTokens.FillTertiary -> fillTertiary diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Shapes.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Shapes.kt index a20dbc9b94..c900899b1f 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Shapes.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Shapes.kt @@ -23,6 +23,7 @@ import com.hedvig.android.design.system.hedvig.FigmaShapeDirection.All import com.hedvig.android.design.system.hedvig.FigmaShapeDirection.BottomOnly import com.hedvig.android.design.system.hedvig.FigmaShapeDirection.TopOnly import com.hedvig.android.design.system.hedvig.tokens.ShapeKeyTokens +import com.hedvig.android.design.system.hedvig.tokens.ShapeKeyTokens.CornerFull import com.hedvig.android.design.system.hedvig.tokens.ShapeKeyTokens.CornerLarge import com.hedvig.android.design.system.hedvig.tokens.ShapeKeyTokens.CornerMedium import com.hedvig.android.design.system.hedvig.tokens.ShapeKeyTokens.CornerNone @@ -60,6 +61,7 @@ internal val ShapeKeyTokens.value: Shape internal fun Shapes.fromToken(token: ShapeKeyTokens): Shape { return when (token) { + CornerFull -> cornerFull CornerXXLarge -> cornerXXLarge CornerXLarge -> cornerXLarge CornerLarge -> cornerLarge diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ButtonTokens.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ButtonTokens.kt index a665012f79..bc6595345f 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ButtonTokens.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ButtonTokens.kt @@ -70,6 +70,30 @@ internal object RedStyleButtonTokens { val RedContentColor = ColorSchemeKeyTokens.SignalRedElement // should not be used ever } +internal object RoundedPrimaryStyleButtonTokens { + val ContainerColor = ColorSchemeKeyTokens.ButtonRoundedPrimaryResting + val ContentColor = ColorSchemeKeyTokens.ButtonRoundedPrimaryContent + val HoverContainerColor = ColorSchemeKeyTokens.ButtonRoundedPrimaryHover + val HoverContentColor = ColorSchemeKeyTokens.ButtonRoundedPrimaryHoverContent + val DisabledContainerColor = ColorSchemeKeyTokens.ButtonRoundedPrimaryDisabled + val DisabledContentColor = ColorSchemeKeyTokens.ButtonRoundedPrimaryDisabledContent + val ActiveLoadingIndicatorColor = ColorSchemeKeyTokens.ButtonRoundedPrimaryLoadingActive + val InactiveLoadingIndicatorColor = ColorSchemeKeyTokens.ButtonRoundedPrimaryLoadingInactive + val RedContentColor = ColorSchemeKeyTokens.ButtonRoundedPrimaryRedContent +} + +internal object RoundedLiquidGlassStyleButtonTokens { + val ContainerColor = ColorSchemeKeyTokens.ButtonRoundedLiquidGlassResting + val ContentColor = ColorSchemeKeyTokens.ButtonRoundedLiquidGlassContent + val HoverContainerColor = ColorSchemeKeyTokens.ButtonRoundedLiquidGlassHover + val HoverContentColor = ColorSchemeKeyTokens.ButtonRoundedLiquidGlassHoverContent + val DisabledContainerColor = ColorSchemeKeyTokens.ButtonRoundedLiquidGlassDisabled + val DisabledContentColor = ColorSchemeKeyTokens.ButtonRoundedLiquidGlassDisabledContent + val ActiveLoadingIndicatorColor = ColorSchemeKeyTokens.ButtonRoundedLiquidGlassLoadingActive + val InactiveLoadingIndicatorColor = ColorSchemeKeyTokens.ButtonRoundedLiquidGlassLoadingInactive + val RedContentColor = ColorSchemeKeyTokens.ButtonRoundedLiquidGlassRedContent +} + internal object LargeSizeButtonTokens { val HorizontalPadding = 32.dp val TopPadding = 15.dp @@ -78,6 +102,16 @@ internal object LargeSizeButtonTokens { val LabelTextFont = TypographyKeyTokens.BodySmall } +// TODO: the rounded button styles are only designed at the large size. Every smaller size falls back to +// the standard button metrics above until design provides them. +internal object RoundedLargeSizeButtonTokens { + val HorizontalPadding = 20.dp + val TopPadding = 12.dp + val BottomPadding = 12.dp + val ContainerShape = ShapeKeyTokens.CornerFull + val LabelTextFont = TypographyKeyTokens.BodySmall +} + internal object MediumSizeButtonTokens { val HorizontalPadding = 14.dp val TopPadding = 7.dp // todo look into if we use the right line heights to justify the offset introduced here diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorDarkTokens.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorDarkTokens.kt index 4861801346..f12b8cc12d 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorDarkTokens.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorDarkTokens.kt @@ -36,6 +36,31 @@ internal object ColorDarkTokens { val ButtonGhostResting: Color = PaletteTokens.Transparent val ButtonGhostHover: Color = PaletteTokens.G900TDark val ButtonGhostDisabled: Color = PaletteTokens.Transparent + + // TODO: the rounded buttons are only designed for light mode. In dark mode the primary one inverts to + // an opaque white fill and the remaining states mirror the primary button. Review once design catches up. + val ButtonRoundedPrimaryResting: Color = PaletteTokens.White + val ButtonRoundedPrimaryHover: Color = PaletteTokens.G200TLight + val ButtonRoundedPrimaryDisabled: Color = PaletteTokens.G900 + val ButtonRoundedPrimaryContent: Color = PaletteTokens.Black + val ButtonRoundedPrimaryHoverContent: Color = PaletteTokens.Black + val ButtonRoundedPrimaryDisabledContent: Color = PaletteTokens.G700 + val ButtonRoundedPrimaryLoadingActive: Color = PaletteTokens.Black + val ButtonRoundedPrimaryLoadingInactive: Color = PaletteTokens.G800TDark + val ButtonRoundedPrimaryRedContent: Color = PaletteTokens.Red650 + + // TODO: the rounded buttons are only designed for light mode. In dark mode the liquid glass one is a + // copy of the secondary button. Review once design catches up. + val ButtonRoundedLiquidGlassResting: Color = PaletteTokens.G900TDark + val ButtonRoundedLiquidGlassHover: Color = PaletteTokens.G900TDark + val ButtonRoundedLiquidGlassDisabled: Color = PaletteTokens.G900 + val ButtonRoundedLiquidGlassContent: Color = PaletteTokens.White + val ButtonRoundedLiquidGlassHoverContent: Color = PaletteTokens.White + val ButtonRoundedLiquidGlassDisabledContent: Color = PaletteTokens.G700 + val ButtonRoundedLiquidGlassLoadingActive: Color = PaletteTokens.White + val ButtonRoundedLiquidGlassLoadingInactive: Color = PaletteTokens.G800TDark + val ButtonRoundedLiquidGlassRedContent: Color = PaletteTokens.Red650 + val FillPrimary: Color = PaletteTokens.White val FillSecondary: Color = PaletteTokens.G500 val FillTertiary: Color = PaletteTokens.G700 diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorLightTokens.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorLightTokens.kt index 63978c52bb..72c02be07a 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorLightTokens.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorLightTokens.kt @@ -36,6 +36,31 @@ internal object ColorLightTokens { val ButtonGhostResting: Color = PaletteTokens.Transparent val ButtonGhostHover: Color = PaletteTokens.G100TLight val ButtonGhostDisabled: Color = PaletteTokens.Transparent + + // TODO: only the resting colors of the rounded buttons come from design. Every other state below + // mirrors the primary button and needs reviewing once the design catches up. + val ButtonRoundedPrimaryResting: Color = PaletteTokens.Black + val ButtonRoundedPrimaryHover: Color = PaletteTokens.G900TDark + val ButtonRoundedPrimaryDisabled: Color = PaletteTokens.G100 + val ButtonRoundedPrimaryContent: Color = PaletteTokens.White + val ButtonRoundedPrimaryHoverContent: Color = PaletteTokens.White + val ButtonRoundedPrimaryDisabledContent: Color = PaletteTokens.G500 + val ButtonRoundedPrimaryLoadingActive: Color = PaletteTokens.White + val ButtonRoundedPrimaryLoadingInactive: Color = PaletteTokens.G200TLight + val ButtonRoundedPrimaryRedContent: Color = PaletteTokens.Red600 + + // TODO: only the resting color comes from design. Every other state below mirrors the ghost button + // and needs reviewing once the design catches up. + val ButtonRoundedLiquidGlassResting: Color = PaletteTokens.LiquidGlassFrost + val ButtonRoundedLiquidGlassHover: Color = PaletteTokens.G100TLight + val ButtonRoundedLiquidGlassDisabled: Color = PaletteTokens.Transparent + val ButtonRoundedLiquidGlassContent: Color = PaletteTokens.Black + val ButtonRoundedLiquidGlassHoverContent: Color = PaletteTokens.Black + val ButtonRoundedLiquidGlassDisabledContent: Color = PaletteTokens.G500 + val ButtonRoundedLiquidGlassLoadingActive: Color = PaletteTokens.Black + val ButtonRoundedLiquidGlassLoadingInactive: Color = PaletteTokens.G200TLight + val ButtonRoundedLiquidGlassRedContent: Color = PaletteTokens.Red600 + val FillPrimary: Color = PaletteTokens.Black val FillSecondary: Color = PaletteTokens.G700 val FillTertiary: Color = PaletteTokens.G500 diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorSchemeKeyTokens.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorSchemeKeyTokens.kt index d138282c7b..d6ce52b9d3 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorSchemeKeyTokens.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorSchemeKeyTokens.kt @@ -35,6 +35,24 @@ internal enum class ColorSchemeKeyTokens { ButtonGhostResting, ButtonGhostHover, ButtonGhostDisabled, + ButtonRoundedPrimaryResting, + ButtonRoundedPrimaryHover, + ButtonRoundedPrimaryDisabled, + ButtonRoundedPrimaryContent, + ButtonRoundedPrimaryHoverContent, + ButtonRoundedPrimaryDisabledContent, + ButtonRoundedPrimaryLoadingActive, + ButtonRoundedPrimaryLoadingInactive, + ButtonRoundedPrimaryRedContent, + ButtonRoundedLiquidGlassResting, + ButtonRoundedLiquidGlassHover, + ButtonRoundedLiquidGlassDisabled, + ButtonRoundedLiquidGlassContent, + ButtonRoundedLiquidGlassHoverContent, + ButtonRoundedLiquidGlassDisabledContent, + ButtonRoundedLiquidGlassLoadingActive, + ButtonRoundedLiquidGlassLoadingInactive, + ButtonRoundedLiquidGlassRedContent, FillPrimary, FillSecondary, FillTertiary, diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/PaletteTokens.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/PaletteTokens.kt index a08009c7df..f4a344fbb0 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/PaletteTokens.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/PaletteTokens.kt @@ -23,6 +23,14 @@ internal object PaletteTokens { val G800 = Color(0xFF505050) val G900 = Color(0xFF303030) + /** + * The translucent fill of the iOS-style glass material. Composited over any backdrop it lands within + * ~1.5% of the frosted white Figma renders for the rounded liquid glass button. + * + * TODO: replace with a real palette entry once the design system defines the glass material. + */ + val LiquidGlassFrost = Color(0xA1F0F0F0) + val G50TLight = Color(0x05121212) val G100TLight = Color(0x0B121212) val G150TLight = Color(0x0D121212) diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ShapeKeyTokens.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ShapeKeyTokens.kt index 9d426a7cb7..633b15ea5b 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ShapeKeyTokens.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ShapeKeyTokens.kt @@ -1,6 +1,7 @@ package com.hedvig.android.design.system.hedvig.tokens internal enum class ShapeKeyTokens { + CornerFull, CornerXXLarge, CornerXLarge, CornerLarge, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index d623808277..cb7f953256 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -72,7 +72,6 @@ import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.positionInParent import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag -import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.semantics @@ -105,6 +104,8 @@ import com.hedvig.android.data.coinsured.CoInsuredFlowType import com.hedvig.android.data.contract.CrossSell import com.hedvig.android.data.contract.ImageAsset import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonStyle.RoundedLiquidGlass +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonStyle.RoundedPrimary import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonStyle.Secondary import com.hedvig.android.design.system.hedvig.DraftClaimDialog import com.hedvig.android.design.system.hedvig.ErrorDialog @@ -1233,22 +1234,6 @@ private fun HomeActionTile(icon: ImageVector, text: String, onClick: () -> Unit, } } -@Composable -private fun HomeActionChip(text: String, onClick: () -> Unit) { - Surface( - onClick = onClick, - shape = HedvigTheme.shapes.cornerXLarge, - color = HedvigTheme.colorScheme.surfacePrimaryTransparent, - role = Role.Button, - ) { - HedvigText( - text = text, - style = HedvigTheme.typography.label, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), - ) - } -} - @Composable private fun QuickActionCarouselSection( isHelpCenterEnabled: Boolean, @@ -1266,11 +1251,26 @@ private fun QuickActionCarouselSection( .padding(horizontal = 16.dp) .padding(horizontalInsets), ) { - HomeActionChip(stringResource(Res.string.home_tab_claim_button_text), onMakeClaim) + HedvigButton( + text = stringResource(Res.string.home_tab_claim_button_text), + onClick = onMakeClaim, + enabled = true, + buttonStyle = RoundedPrimary, + ) if (isHelpCenterEnabled) { - HomeActionChip(stringResource(Res.string.home_tab_get_help), onHelpAndSupport) + HedvigButton( + text = stringResource(Res.string.home_tab_get_help), + onClick = onHelpAndSupport, + enabled = true, + buttonStyle = RoundedLiquidGlass, + ) } - HomeActionChip(stringResource(Res.string.DASHBOARD_OPEN_CHAT), onContactUs) + HedvigButton( + text = stringResource(Res.string.DASHBOARD_OPEN_CHAT), + onClick = onContactUs, + enabled = true, + buttonStyle = RoundedLiquidGlass, + ) } } From 0d9000d28ddc413f5ebed90af9382f9eb5b785c5 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 5 Aug 2026 11:20:36 +0200 Subject: [PATCH 08/23] add white rim --- .../android/design/system/hedvig/Button.kt | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt index 8555ab4980..98721decf9 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt @@ -7,6 +7,7 @@ import androidx.compose.animation.core.updateTransition import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.interaction.MutableInteractionSource @@ -28,6 +29,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.dropShadow import androidx.compose.ui.draw.innerShadow +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.compositeOver @@ -156,6 +158,7 @@ fun HedvigButton( .background(color, shape) .innerShadow(shape, glass.rimShade) .innerShadow(shape, glass.rimSheen) + .then(glass.rimHighlight?.let { Modifier.border(it, shape) } ?: Modifier) }, onClickLabel = onClickLabel, role = Role.Button, @@ -354,12 +357,17 @@ private fun ButtonSize.sizeIn(style: Style): Size = when { } /** - * The container treatment of the iOS glass material: a [dropShadow] behind the container, and two - * inner shadows over its fill that shade the top-left rim and light up the bottom-right one, which is - * how the material reads as lit from below and to the right. + * The container treatment of the iOS glass material: a [dropShadow] behind the container, two inner + * shadows over its fill that shade the top-left rim and light up the bottom-right one, and an optional + * hairline [rimHighlight] where the material catches the light along its top edge. */ @Immutable -private data class GlassContainer(val dropShadow: Shadow, val rimShade: Shadow, val rimSheen: Shadow) +private data class GlassContainer( + val dropShadow: Shadow, + val rimShade: Shadow, + val rimSheen: Shadow, + val rimHighlight: BorderStroke?, +) private val glassDropShadow = Shadow( radius = 40.dp, @@ -376,6 +384,16 @@ private val regularGlassContainer = GlassContainer( dropShadow = glassDropShadow, rimShade = Shadow(radius = 10.dp, color = Color.Black, offset = DpOffset(4.dp, 4.dp), alpha = 0.06f), rimSheen = Shadow(radius = 10.dp, color = Color.White, offset = DpOffset((-4).dp, (-4).dp), alpha = 0.80f), + // Near-opaque along the top edge and down the upper half of the sides, gone by the bottom, where the + // broader rimSheen takes over. + rimHighlight = BorderStroke( + width = 1.dp, + brush = Brush.verticalGradient( + 0f to Color.White.copy(alpha = 0.85f), + 0.6f to Color.White.copy(alpha = 0.70f), + 1f to Color.White.copy(alpha = 0f), + ), + ), ) /** The rim weighted for an opaque near-black fill, which would read as a grey smudge under [regularGlassContainer]. */ @@ -383,6 +401,7 @@ private val opaqueDarkGlassContainer = GlassContainer( dropShadow = glassDropShadow, rimShade = Shadow(radius = 10.dp, color = Color.Black, offset = DpOffset(4.dp, 4.dp), alpha = 0.40f), rimSheen = Shadow(radius = 10.dp, color = Color.White, offset = DpOffset((-4).dp, (-4).dp), alpha = 0.04f), + rimHighlight = null, ) @Immutable From 5599c8c91e0b12d5cff9b4ba1448cd951892c6df Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 5 Aug 2026 11:29:16 +0200 Subject: [PATCH 09/23] separate liquid glass, apply to toolbar icons --- .../android/design/system/hedvig/Button.kt | 69 ++------------- .../design/system/hedvig/ColorScheme.kt | 4 + .../design/system/hedvig/LiquidGlass.kt | 83 +++++++++++++++++++ .../system/hedvig/tokens/ColorDarkTokens.kt | 1 + .../system/hedvig/tokens/ColorLightTokens.kt | 1 + .../hedvig/tokens/ColorSchemeKeyTokens.kt | 1 + .../feature/home/home/ui/ToolbarIcons.kt | 6 +- 7 files changed, 100 insertions(+), 65 deletions(-) create mode 100644 app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/LiquidGlass.kt diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt index 98721decf9..be54f8239d 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt @@ -147,18 +147,13 @@ fun HedvigButton( @Suppress("NAME_SHADOWING") val shape = shape ?: size.shape - val glass = style.glassContainer.takeIf { enabled } + val glass = style.glassMaterial.takeIf { enabled } Surface( onClick = onClick, modifier = if (glass == null) { modifier } else { - modifier - .dropShadow(shape, glass.dropShadow) - .background(color, shape) - .innerShadow(shape, glass.rimShade) - .innerShadow(shape, glass.rimSheen) - .then(glass.rimHighlight?.let { Modifier.border(it, shape) } ?: Modifier) + modifier.glassMaterial(glass, color, shape) }, onClickLabel = onClickLabel, role = Role.Button, @@ -352,58 +347,10 @@ private val ButtonSize.size: Size */ @Composable private fun ButtonSize.sizeIn(style: Style): Size = when { - style.glassContainer != null && this == ButtonSize.Large -> Size.LargeRounded + style.glassMaterial != null && this == ButtonSize.Large -> Size.LargeRounded else -> size } -/** - * The container treatment of the iOS glass material: a [dropShadow] behind the container, two inner - * shadows over its fill that shade the top-left rim and light up the bottom-right one, and an optional - * hairline [rimHighlight] where the material catches the light along its top edge. - */ -@Immutable -private data class GlassContainer( - val dropShadow: Shadow, - val rimShade: Shadow, - val rimSheen: Shadow, - val rimHighlight: BorderStroke?, -) - -private val glassDropShadow = Shadow( - radius = 40.dp, - color = Color.Black, - offset = DpOffset(0.dp, 8.dp), - alpha = 0.12f, -) - -// TODO: the rim shadows below stand in for a Figma glass effect that cannot be exported, so their -// values are read off the Figma render rather than given by design. Revisit once design catches up. - -/** The rim as the glass material normally reads: a faint shade and a pronounced sheen. */ -private val regularGlassContainer = GlassContainer( - dropShadow = glassDropShadow, - rimShade = Shadow(radius = 10.dp, color = Color.Black, offset = DpOffset(4.dp, 4.dp), alpha = 0.06f), - rimSheen = Shadow(radius = 10.dp, color = Color.White, offset = DpOffset((-4).dp, (-4).dp), alpha = 0.80f), - // Near-opaque along the top edge and down the upper half of the sides, gone by the bottom, where the - // broader rimSheen takes over. - rimHighlight = BorderStroke( - width = 1.dp, - brush = Brush.verticalGradient( - 0f to Color.White.copy(alpha = 0.85f), - 0.6f to Color.White.copy(alpha = 0.70f), - 1f to Color.White.copy(alpha = 0f), - ), - ), -) - -/** The rim weighted for an opaque near-black fill, which would read as a grey smudge under [regularGlassContainer]. */ -private val opaqueDarkGlassContainer = GlassContainer( - dropShadow = glassDropShadow, - rimShade = Shadow(radius = 10.dp, color = Color.Black, offset = DpOffset(4.dp, 4.dp), alpha = 0.40f), - rimSheen = Shadow(radius = 10.dp, color = Color.White, offset = DpOffset((-4).dp, (-4).dp), alpha = 0.04f), - rimHighlight = null, -) - @Immutable private data class ButtonColors( val containerColor: Color, @@ -544,7 +491,7 @@ private sealed interface Style { val buttonColors: ButtonColors /** Non-null on the styles that render the iOS glass material. */ - val glassContainer: GlassContainer? + val glassMaterial: GlassMaterial? @Composable get() = null @@ -650,9 +597,9 @@ private sealed interface Style { data object RoundedPrimary : Style { // The fill inverts between themes: near-black on light, opaque white on dark. - override val glassContainer: GlassContainer + override val glassMaterial: GlassMaterial @Composable - get() = if (HedvigTheme.colorScheme.isLight) opaqueDarkGlassContainer else regularGlassContainer + get() = if (HedvigTheme.colorScheme.isLight) opaqueDarkGlassMaterial else regularGlassMaterial override val buttonColors: ButtonColors @Composable @@ -674,9 +621,9 @@ private sealed interface Style { } data object RoundedLiquidGlass : Style { - override val glassContainer: GlassContainer + override val glassMaterial: GlassMaterial @Composable - get() = regularGlassContainer + get() = regularGlassMaterial override val buttonColors: ButtonColors @Composable diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ColorScheme.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ColorScheme.kt index fadf70c8a7..7284d053ee 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ColorScheme.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ColorScheme.kt @@ -88,6 +88,7 @@ data class ColorScheme( val surfacePrimaryTransparent: Color, val surfaceSecondaryTransparent: Color, val surfaceHighlightTransparent: Color, + val surfaceLiquidGlass: Color, val backgroundPrimary: Color, val backgroundNegative: Color, val backgroundBlack: Color, @@ -215,6 +216,7 @@ internal val lightColorScheme: ColorScheme = ColorScheme( surfacePrimaryTransparent = ColorLightTokens.SurfacePrimaryTransparent, surfaceSecondaryTransparent = ColorLightTokens.SurfaceSecondaryTransparent, surfaceHighlightTransparent = ColorLightTokens.SurfaceHighlightTransparent, + surfaceLiquidGlass = ColorLightTokens.SurfaceLiquidGlass, backgroundPrimary = ColorLightTokens.BackgroundPrimary, backgroundNegative = ColorLightTokens.BackgroundNegative, backgroundBlack = ColorLightTokens.BackgroundBlack, @@ -342,6 +344,7 @@ internal val darkColorScheme: ColorScheme = ColorScheme( surfacePrimaryTransparent = ColorDarkTokens.SurfacePrimaryTransparent, surfaceSecondaryTransparent = ColorDarkTokens.SurfaceSecondaryTransparent, surfaceHighlightTransparent = ColorDarkTokens.SurfaceHighlightTransparent, + surfaceLiquidGlass = ColorDarkTokens.SurfaceLiquidGlass, backgroundPrimary = ColorDarkTokens.BackgroundPrimary, backgroundNegative = ColorDarkTokens.BackgroundNegative, backgroundBlack = ColorDarkTokens.BackgroundBlack, @@ -494,6 +497,7 @@ internal fun ColorScheme.fromToken(value: ColorSchemeKeyTokens): Color { ColorSchemeKeyTokens.SurfacePrimaryTransparent -> surfacePrimaryTransparent ColorSchemeKeyTokens.SurfaceSecondaryTransparent -> surfaceSecondaryTransparent ColorSchemeKeyTokens.SurfaceHighlightTransparent -> surfaceHighlightTransparent + ColorSchemeKeyTokens.SurfaceLiquidGlass -> surfaceLiquidGlass ColorSchemeKeyTokens.BackgroundPrimary -> backgroundPrimary ColorSchemeKeyTokens.BackgroundNegative -> backgroundNegative ColorSchemeKeyTokens.BackgroundBlack -> backgroundBlack diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/LiquidGlass.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/LiquidGlass.kt new file mode 100644 index 0000000000..93b666f9e3 --- /dev/null +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/LiquidGlass.kt @@ -0,0 +1,83 @@ +package com.hedvig.android.design.system.hedvig + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.dropShadow +import androidx.compose.ui.draw.innerShadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp + +/** + * Fills [shape] with the iOS-style glass material: a translucent fill that lets the backdrop through, a + * drop shadow, rim shading, and a hairline highlight along the top edge. + * + * Draws behind the content it is applied to, so put it before any `clickable` that should ripple on top + * of it. + */ +@Composable +fun Modifier.liquidGlass(shape: Shape): Modifier = + glassMaterial(regularGlassMaterial, HedvigTheme.colorScheme.surfaceLiquidGlass, shape) + +/** + * The rim and shadow treatment of the glass material: a [dropShadow] behind the container, two inner + * shadows over its fill that shade the top-left rim and light up the bottom-right one, and an optional + * hairline [rimHighlight] where the material catches the light along its top edge. + */ +@Immutable +internal data class GlassMaterial( + val dropShadow: Shadow, + val rimShade: Shadow, + val rimSheen: Shadow, + val rimHighlight: BorderStroke?, +) + +/** Paints [material] over a [fill] container, both clipped to [shape]. */ +internal fun Modifier.glassMaterial(material: GlassMaterial, fill: Color, shape: Shape): Modifier = this + .dropShadow(shape, material.dropShadow) + .background(fill, shape) + .innerShadow(shape, material.rimShade) + .innerShadow(shape, material.rimSheen) + .then(material.rimHighlight?.let { Modifier.border(it, shape) } ?: Modifier) + +private val glassDropShadow = Shadow( + radius = 40.dp, + color = Color.Black, + offset = DpOffset(0.dp, 8.dp), + alpha = 0.12f, +) + +// TODO: the rim shadows below stand in for a Figma glass effect that cannot be exported, so their +// values are read off the Figma render rather than given by design. Revisit once design catches up. + +/** The rim as the glass material normally reads: a faint shade and a pronounced sheen. */ +internal val regularGlassMaterial = GlassMaterial( + dropShadow = glassDropShadow, + rimShade = Shadow(radius = 10.dp, color = Color.Black, offset = DpOffset(4.dp, 4.dp), alpha = 0.06f), + rimSheen = Shadow(radius = 10.dp, color = Color.White, offset = DpOffset((-4).dp, (-4).dp), alpha = 0.80f), + // Near-opaque along the top edge and down the upper half of the sides, gone by the bottom, where the + // broader rimSheen takes over. + rimHighlight = BorderStroke( + width = 1.dp, + brush = Brush.verticalGradient( + 0f to Color.White.copy(alpha = 0.85f), + 0.6f to Color.White.copy(alpha = 0.70f), + 1f to Color.White.copy(alpha = 0f), + ), + ), +) + +/** The rim weighted for an opaque near-black fill, which would read as a grey smudge under [regularGlassMaterial]. */ +internal val opaqueDarkGlassMaterial = GlassMaterial( + dropShadow = glassDropShadow, + rimShade = Shadow(radius = 10.dp, color = Color.Black, offset = DpOffset(4.dp, 4.dp), alpha = 0.40f), + rimSheen = Shadow(radius = 10.dp, color = Color.White, offset = DpOffset((-4).dp, (-4).dp), alpha = 0.04f), + rimHighlight = null, +) diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorDarkTokens.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorDarkTokens.kt index f12b8cc12d..ae49ba9577 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorDarkTokens.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorDarkTokens.kt @@ -80,6 +80,7 @@ internal object ColorDarkTokens { val SurfacePrimaryTransparent: Color = PaletteTokens.G900TDark val SurfaceSecondaryTransparent: Color = PaletteTokens.G800TDark val SurfaceHighlightTransparent: Color = PaletteTokens.G900TDark + val SurfaceLiquidGlass: Color = PaletteTokens.G900TDark val BackgroundPrimary: Color = PaletteTokens.Black val BackgroundNegative: Color = PaletteTokens.White val BackgroundBlack: Color = PaletteTokens.Black diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorLightTokens.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorLightTokens.kt index 72c02be07a..fb5dc03711 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorLightTokens.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorLightTokens.kt @@ -80,6 +80,7 @@ internal object ColorLightTokens { val SurfacePrimaryTransparent: Color = PaletteTokens.G100TLight val SurfaceSecondaryTransparent: Color = PaletteTokens.G200TLight val SurfaceHighlightTransparent: Color = PaletteTokens.G50TLight + val SurfaceLiquidGlass: Color = PaletteTokens.LiquidGlassFrost val BackgroundPrimary: Color = PaletteTokens.White val BackgroundNegative: Color = PaletteTokens.Black val BackgroundBlack: Color = PaletteTokens.Black diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorSchemeKeyTokens.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorSchemeKeyTokens.kt index d6ce52b9d3..792329ec62 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorSchemeKeyTokens.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/tokens/ColorSchemeKeyTokens.kt @@ -72,6 +72,7 @@ internal enum class ColorSchemeKeyTokens { SurfacePrimaryTransparent, SurfaceSecondaryTransparent, SurfaceHighlightTransparent, + SurfaceLiquidGlass, BackgroundPrimary, BackgroundNegative, BackgroundBlack, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/ToolbarIcons.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/ToolbarIcons.kt index b9e4c375c3..34a3bc7a34 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/ToolbarIcons.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/ToolbarIcons.kt @@ -3,7 +3,6 @@ package com.hedvig.android.feature.home.home.ui import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -18,7 +17,6 @@ import androidx.compose.runtime.produceState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.semantics.Role @@ -34,6 +32,7 @@ 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.colored.ColoredChat import com.hedvig.android.design.system.hedvig.icon.colored.ColoredFirstVetNoCircle +import com.hedvig.android.design.system.hedvig.liquidGlass import hedvig.resources.DASHBOARD_OPEN_CHAT import hedvig.resources.HC_QUICK_ACTIONS_FIRSTVET_SUBTITLE import hedvig.resources.Res @@ -55,9 +54,8 @@ private fun ToolbarIconButton(onClick: () -> Unit, modifier: Modifier = Modifier contentAlignment = Alignment.Center, modifier = modifier .size(toolbarButtonSize) - .shadow(4.dp, CircleShape) + .liquidGlass(CircleShape) .clip(CircleShape) - .background(HedvigTheme.colorScheme.surfacePrimary) .clickable(role = Role.Button, onClick = onClick), content = { glyph() }, ) From 9d37968d8f65e9546e10c00c17558e067851a2e1 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 5 Aug 2026 11:53:50 +0200 Subject: [PATCH 10/23] change rimSheen a little bit --- .../com/hedvig/android/design/system/hedvig/LiquidGlass.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/LiquidGlass.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/LiquidGlass.kt index 93b666f9e3..f739b2728f 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/LiquidGlass.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/LiquidGlass.kt @@ -61,7 +61,9 @@ private val glassDropShadow = Shadow( internal val regularGlassMaterial = GlassMaterial( dropShadow = glassDropShadow, rimShade = Shadow(radius = 10.dp, color = Color.Black, offset = DpOffset(4.dp, 4.dp), alpha = 0.06f), - rimSheen = Shadow(radius = 10.dp, color = Color.White, offset = DpOffset((-4).dp, (-4).dp), alpha = 0.80f), + // Cast straight up from the bottom edge: the material lifts along the bottom roughly four times as + // much as along the sides, which a diagonal offset would spread too far around a narrow container. + rimSheen = Shadow(radius = 10.dp, color = Color.White, offset = DpOffset(0.dp, (-4).dp), alpha = 0.80f), // Near-opaque along the top edge and down the upper half of the sides, gone by the bottom, where the // broader rimSheen takes over. rimHighlight = BorderStroke( From d87ccd469bb6bb1b4953b023e722d287f24c23b3 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 5 Aug 2026 11:58:16 +0200 Subject: [PATCH 11/23] feature-home: edit-insurance tile, home-specific copy keys, offers preview - Replace the duplicated "Help & support" quick-action tile with an "Edit insurance" tile (gear icon) wired to the change-coverage flow (StartTierFlowChooseInsuranceKey). Help & support stays in the top pill row. - Point the home quick-action tiles, discover section title/button, and addons button at new HOME_* Lokalise keys instead of shared Help Center / cross-sell keys, so home copy no longer leaks into other screens. - Add an optional buttonText override to CrossSellsSection (defaults to the existing key, so other callers are unchanged). - Render the Offers ("Your quotes") card inside PreviewHomeScreen. --- .../app/navigation/HedvigEntryProvider.kt | 2 + .../androidMain/res/values-sv-rSE/strings.xml | 7 ++ .../src/androidMain/res/values/strings.xml | 7 ++ .../values-sv-rSE/strings.xml | 7 ++ .../composeResources/values/strings.xml | 7 ++ .../home/home/navigation/HomeEntries.kt | 2 + .../feature/home/home/ui/HomeDestination.kt | 67 +++++++++++++------ .../hedvig/android/crosssells/CrossSells.kt | 7 +- 8 files changed, 84 insertions(+), 22 deletions(-) diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt index 7d88f3a499..41c20a4b81 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt @@ -15,6 +15,7 @@ import com.hedvig.android.feature.addon.purchase.navigation.AddonPurchaseKey import com.hedvig.android.feature.addon.purchase.navigation.addonPurchaseEntries import com.hedvig.android.feature.change.tier.navigation.ChooseTierKey import com.hedvig.android.feature.change.tier.navigation.InsuranceCustomizationParameters +import com.hedvig.android.feature.change.tier.navigation.StartTierFlowChooseInsuranceKey import com.hedvig.android.feature.change.tier.navigation.StartTierFlowKey import com.hedvig.android.feature.change.tier.navigation.changeTierEntries import com.hedvig.android.feature.chat.navigation.ChatKey @@ -250,6 +251,7 @@ private fun EntryProviderScope.addHomeEntries( }, navigateToHelpCenter = { backstack.add(HelpCenterKey) }, navigateToMovingFlow = { navigateToMovingFlow(MovingSource.OTHER) }, + navigateToEditInsurance = { backstack.add(StartTierFlowChooseInsuranceKey) }, navigateToClaimChat = { resumeClaim -> backstack.add( ClaimChatKey( 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 1d719d7c34..e39bad0e74 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 @@ -419,7 +419,9 @@ Din försäkring kommer att uppdateras den %1$s. Bekräfta Kontakta vårt serviceteam + Inaktiverad Ladda ner + Aktiverad Felaktig inmatning Vet inte Försök igen @@ -499,8 +501,13 @@ Topic not found This topic has no questions Hedvig + Läs mer + Upptäck våra försäkringar + Se pris Hur kan vi hjälpa dig? Hej %1$s + Ändra adress + Ändra försäkring Dina prisförslag Lägg till medförsäkrad Chip-ID för husdjur saknas 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 a2ba718673..78bfe302f2 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -419,7 +419,9 @@ Your insurance will be updated starting from %1$s Confirm Contact our service team + Disabled Download + Enabled Invalid input Not sure Try again @@ -499,8 +501,13 @@ Topic not found This topic has no questions Hedvig + Read more + Discover our insurances + See price How can we help? Hi %1$s + Change address + Edit insurance Your quotes Add co-insured Missing pet chip-ID 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 b55551902e..a5c1e20461 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 @@ -418,7 +418,9 @@ Din försäkring kommer att uppdateras den %1$s. Bekräfta Kontakta vårt serviceteam + Inaktiverad Ladda ner + Aktiverad Felaktig inmatning Vet inte Försök igen @@ -498,8 +500,13 @@ Topic not found This topic has no questions Hedvig + Läs mer + Upptäck våra försäkringar + Se pris Hur kan vi hjälpa dig? Hej %1$s + Ändra adress + Ändra försäkring Dina prisförslag Lägg till medförsäkrad Chip-ID för husdjur saknas 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 6f8d4811df..ec68bccd0c 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -418,7 +418,9 @@ Your insurance will be updated starting from %1$s Confirm Contact our service team + Disabled Download + Enabled Invalid input Not sure Try again @@ -498,8 +500,13 @@ Topic not found This topic has no questions Hedvig + Read more + Discover our insurances + See price How can we help? Hi %1$s + Change address + Edit insurance Your quotes Add co-insured Missing pet chip-ID diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt index bedff77991..452946d427 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt @@ -26,6 +26,7 @@ fun EntryProviderScope.homeEntries( navigateToMissingInfo: (String, CoInsuredFlowType) -> Unit, navigateToHelpCenter: () -> Unit, navigateToMovingFlow: () -> Unit, + navigateToEditInsurance: () -> Unit, navigateToClaimChat: (resumeClaim: Boolean) -> Unit, navigateToChipIdScreen: () -> Unit, openAppSettings: () -> Unit, @@ -50,6 +51,7 @@ fun EntryProviderScope.homeEntries( navigateToMissingInfo = dropUnlessResumed { contractId, type -> navigateToMissingInfo(contractId, type) }, navigateToHelpCenter = dropUnlessResumed { navigateToHelpCenter() }, navigateToMovingFlow = dropUnlessResumed { navigateToMovingFlow() }, + navigateToEditInsurance = dropUnlessResumed { navigateToEditInsurance() }, openUrl = openUrl, openCrossSellUrl = openCrossSellUrl, openAppSettings = openAppSettings, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 1b17bbbf20..2c0a8b1ebd 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -123,8 +123,8 @@ import com.hedvig.android.design.system.hedvig.TopAppBarLayoutForActions import com.hedvig.android.design.system.hedvig.api.HedvigBottomSheetState import com.hedvig.android.design.system.hedvig.hedvigDropShadow import com.hedvig.android.design.system.hedvig.icon.HedvigIcons -import com.hedvig.android.design.system.hedvig.icon.HelipadOutline import com.hedvig.android.design.system.hedvig.icon.Reload +import com.hedvig.android.design.system.hedvig.icon.Settings import com.hedvig.android.design.system.hedvig.icon.Travel import com.hedvig.android.design.system.hedvig.notificationCircle import com.hedvig.android.design.system.hedvig.rememberHedvigBottomSheetState @@ -163,15 +163,17 @@ import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment.SegmentText. import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment.SegmentType.INACTIVE import com.hedvig.android.ui.claimstatus.model.ClaimStatusCardUiState import com.hedvig.android.ui.emergency.FirstVetSection -import hedvig.resources.ADDON_FLOW_LEARN_MORE_BUTTON import hedvig.resources.CHAT_NEW_MESSAGE -import hedvig.resources.CROSS_SELL_SUBTITLE import hedvig.resources.DASHBOARD_OPEN_CHAT import hedvig.resources.HC_QUICK_ACTIONS_TITLE import hedvig.resources.HC_QUICK_ACTIONS_TRAVEL_CERTIFICATE -import hedvig.resources.HC_QUICK_ACTIONS_UPDATE_ADDRESS +import hedvig.resources.HOME_ADDONS_READ_MORE_BUTTON +import hedvig.resources.HOME_DISCOVER_SECTION_TITLE +import hedvig.resources.HOME_DISCOVER_SEE_PRICE_BUTTON import hedvig.resources.HOME_GREETING_SUBTITLE import hedvig.resources.HOME_GREETING_TITLE +import hedvig.resources.HOME_QUICK_ACTIONS_CHANGE_ADDRESS +import hedvig.resources.HOME_QUICK_ACTIONS_EDIT_INSURANCE import hedvig.resources.HOME_QUOTES_SECTION_TITLE import hedvig.resources.HOME_TODO_SECTION_TITLE import hedvig.resources.INSURANCE_ADDONS_SUBHEADING @@ -212,6 +214,7 @@ internal fun HomeDestination( navigateToConnectPayout: () -> Unit, navigateToHelpCenter: () -> Unit, navigateToMovingFlow: () -> Unit, + navigateToEditInsurance: () -> Unit, openUrl: (String) -> Unit, openCrossSellUrl: (String) -> Unit, openAppSettings: () -> Unit, @@ -237,6 +240,7 @@ internal fun HomeDestination( navigateToConnectPayout = navigateToConnectPayout, navigateToHelpCenter = navigateToHelpCenter, navigateToMovingFlow = navigateToMovingFlow, + navigateToEditInsurance = navigateToEditInsurance, openUrl = openUrl, openCrossSellUrl = openCrossSellUrl, openAppSettings = openAppSettings, @@ -269,6 +273,7 @@ private fun HomeScreen( navigateToConnectPayout: () -> Unit, navigateToHelpCenter: () -> Unit, navigateToMovingFlow: () -> Unit, + navigateToEditInsurance: () -> Unit, openUrl: (String) -> Unit, openCrossSellUrl: (String) -> Unit, markMessageAsSeen: (String) -> Unit, @@ -386,6 +391,7 @@ private fun HomeScreen( navigateToConnectPayout = navigateToConnectPayout, navigateToHelpCenter = navigateToHelpCenter, navigateToMovingFlow = navigateToMovingFlow, + navigateToEditInsurance = navigateToEditInsurance, onNavigateToInbox = onNavigateToInbox, openClaimFlowSheet = { if (draftClaim != null) { @@ -560,6 +566,7 @@ private fun HomeScreenSuccess( navigateToConnectPayout: () -> Unit, navigateToHelpCenter: () -> Unit, navigateToMovingFlow: () -> Unit, + navigateToEditInsurance: () -> Unit, onNavigateToInbox: () -> Unit, openClaimFlowSheet: () -> Unit, onContinueDraftClaim: () -> Unit, @@ -893,8 +900,7 @@ private fun HomeScreenSuccess( ) HomeSection.QuickActionTiles -> QuickActionTilesSection( - isHelpCenterEnabled = uiState.isHelpCenterEnabled, - onHelpAndSupport = navigateToHelpCenter, + onEditInsurance = navigateToEditInsurance, onChangeAddress = navigateToMovingFlow, onTravelCertificate = navigateToTravelCertificate, horizontalInsets = horizontalInsets, @@ -1157,8 +1163,7 @@ private fun OffersSection( @Composable private fun QuickActionTilesSection( - isHelpCenterEnabled: Boolean, - onHelpAndSupport: () -> Unit, + onEditInsurance: () -> Unit, onChangeAddress: () -> Unit, onTravelCertificate: () -> Unit, horizontalInsets: PaddingValues, @@ -1181,19 +1186,17 @@ private fun QuickActionTilesSection( .fillMaxWidth() .height(IntrinsicSize.Max), ) { - if (isHelpCenterEnabled) { - HomeActionTile( - icon = HedvigIcons.HelipadOutline, - text = stringResource(Res.string.home_tab_get_help), - onClick = onHelpAndSupport, - modifier = Modifier - .weight(1f) - .fillMaxHeight(), - ) - } + HomeActionTile( + icon = HedvigIcons.Settings, + text = stringResource(Res.string.HOME_QUICK_ACTIONS_EDIT_INSURANCE), + onClick = onEditInsurance, + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + ) HomeActionTile( icon = HedvigIcons.Reload, - text = stringResource(Res.string.HC_QUICK_ACTIONS_UPDATE_ADDRESS), + text = stringResource(Res.string.HOME_QUICK_ACTIONS_CHANGE_ADDRESS), onClick = onChangeAddress, modifier = Modifier .weight(1f) @@ -1302,7 +1305,7 @@ private fun AddonsSection( subtitle = addon.description, pillowImage = null, pillow = { AddonPillow(addon.flowType) }, - buttonText = stringResource(Res.string.ADDON_FLOW_LEARN_MORE_BUTTON), + buttonText = stringResource(Res.string.HOME_ADDONS_READ_MORE_BUTTON), onButtonClick = { navigateToAddonPurchaseFlow(addon.eligibleInsurancesIds) }, imageLoader = imageLoader, modifier = Modifier.fillMaxWidth(), @@ -1320,12 +1323,13 @@ private fun DiscoverInsurancesSection( imageLoader: ImageLoader, ) { CrossSellsSection( - title = stringResource(Res.string.CROSS_SELL_SUBTITLE), + title = stringResource(Res.string.HOME_DISCOVER_SECTION_TITLE), crossSells = crossSells, onCrossSellClick = onCrossSellClick, modifier = Modifier.padding(horizontal = 16.dp), onSheetDismissed = {}, imageLoader = imageLoader, + buttonText = stringResource(Res.string.HOME_DISCOVER_SEE_PRICE_BUTTON), buttonSize = ButtonSize.Small, buttonShape = HedvigTheme.shapes.cornerFull, ) @@ -1432,6 +1436,24 @@ private fun PreviewHomeScreen( ), isHelpCenterEnabled = true, hasUnseenChatMessages = hasUnseenChatMessages, + crossSellsPartition = CrossSellsPartition( + offersCrossSell = RecommendedCrossSell( + crossSell = CrossSell( + id = "car", + title = "Car Insurance", + subtitle = "For you and your car", + storeUrl = "", + pillowImage = ImageAsset("", "", ""), + ), + bannerText = "15% bundle discount", + buttonText = "See your price", + discountText = "-15%", + buttonDescription = "", + backgroundPillowImages = "" to "", + bundleProgress = null, + ), + discoverCrossSells = emptyList(), + ), crossSellsAction = CrossSellsAction( CrossSellSheetData( recommendedCrossSell = RecommendedCrossSell( @@ -1498,6 +1520,7 @@ private fun PreviewHomeScreen( navigateToConnectPayout = {}, navigateToHelpCenter = {}, navigateToMovingFlow = {}, + navigateToEditInsurance = {}, openUrl = {}, openCrossSellUrl = {}, openAppSettings = {}, @@ -1534,6 +1557,7 @@ private fun PreviewHomeScreenWithError() { navigateToConnectPayout = {}, navigateToHelpCenter = {}, navigateToMovingFlow = {}, + navigateToEditInsurance = {}, openUrl = {}, openCrossSellUrl = {}, openAppSettings = {}, @@ -1592,6 +1616,7 @@ private fun PreviewHomeScreenAllHomeTextTypes( navigateToConnectPayout = {}, navigateToHelpCenter = {}, navigateToMovingFlow = {}, + navigateToEditInsurance = {}, openUrl = {}, openCrossSellUrl = {}, openAppSettings = {}, diff --git a/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt b/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt index f6546b48f3..0692785644 100644 --- a/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt +++ b/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt @@ -610,6 +610,7 @@ fun CrossSellsSection( withSubHeader: Boolean = true, hasCrossSellDiscounts: Boolean = false, title: String? = null, + buttonText: String? = null, buttonSize: ButtonDefaults.ButtonSize = ButtonDefaults.ButtonSize.Medium, buttonShape: Shape? = null, ) { @@ -638,6 +639,7 @@ fun CrossSellsSection( onCrossSellClick, onSheetDismissed = onSheetDismissed, imageLoader = imageLoader, + buttonText = buttonText, buttonSize = buttonSize, buttonShape = buttonShape, ) @@ -685,6 +687,7 @@ private fun CrossSellItem( onSheetDismissed: () -> Unit, imageLoader: ImageLoader, modifier: Modifier = Modifier, + buttonText: String? = null, buttonSize: ButtonDefaults.ButtonSize = ButtonDefaults.ButtonSize.Medium, buttonShape: Shape? = null, ) { @@ -698,6 +701,7 @@ private fun CrossSellItem( imageLoader = imageLoader, crossSellImageAsset = crossSell.pillowImage, onSheetDismissed = onSheetDismissed, + buttonText = buttonText, buttonSize = buttonSize, buttonShape = buttonShape, ) @@ -714,6 +718,7 @@ private fun CrossSellItem( imageLoader: ImageLoader, onSheetDismissed: () -> Unit, modifier: Modifier = Modifier, + buttonText: String? = null, buttonSize: ButtonDefaults.ButtonSize = ButtonDefaults.ButtonSize.Medium, buttonShape: Shape? = null, ) { @@ -721,7 +726,7 @@ private fun CrossSellItem( title = crossSellTitle, subtitle = crossSellSubtitle, pillowImage = crossSellImageAsset, - buttonText = stringResource(Res.string.cross_sell_get_price), + buttonText = buttonText ?: stringResource(Res.string.cross_sell_get_price), onButtonClick = { onCrossSellClick(storeUrl) onSheetDismissed() From 27319fa96f2c6f921b901a0ac9203efcc271feb5 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 5 Aug 2026 14:04:13 +0200 Subject: [PATCH 12/23] feature-home: gate Edit insurance tile on change-tier eligibility Only show the "Edit insurance" quick-action tile when the member can actually change tier, matching how Help Center gates the same destination. Reaching the change-tier flow otherwise lands on its Failure screen. - Request isChangeTierEnabled on the memberActions block QueryHome already fetches. - Map it into HomeData.isEditInsuranceEnabled and thread it through the HomePresenter state (mirroring the existing isHelpCenterEnabled pipeline). - Gate the tile in QuickActionTilesSection on the new flag. - Update the demo use case, previews, and HomePresenterTest. --- .../src/main/graphql/QueryHome.graphql | 1 + .../home/home/data/GetHomeDataUseCase.kt | 3 +++ .../home/home/data/GetHomeDataUseCaseDemo.kt | 1 + .../feature/home/home/ui/HomeDestination.kt | 22 ++++++++++++------- .../feature/home/home/ui/HomePresenter.kt | 5 +++++ .../feature/home/home/ui/HomePresenterTest.kt | 17 ++++++++++++++ 6 files changed, 41 insertions(+), 8 deletions(-) diff --git a/app/feature/feature-home/src/main/graphql/QueryHome.graphql b/app/feature/feature-home/src/main/graphql/QueryHome.graphql index 0d29fa7dd7..08b4754b9e 100644 --- a/app/feature/feature-home/src/main/graphql/QueryHome.graphql +++ b/app/feature/feature-home/src/main/graphql/QueryHome.graphql @@ -81,6 +81,7 @@ query Home($claimsHistoryFlag: Boolean!, $resumeClaimEnabled: Boolean!) { } } memberActions { + isChangeTierEnabled firstVetAction { sections { title diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt index e884924629..d5921acdf3 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt @@ -200,6 +200,7 @@ internal class GetHomeDataUseCaseImpl( memberReminders = memberReminders, hasUnseenChatMessages = hasUnseenChatMessages, showHelpCenter = true, + isEditInsuranceEnabled = homeQueryData.currentMember.memberActions?.isChangeTierEnabled ?: false, firstVetSections = firstVetActions, crossSells = crossSells, addonBannerInfos = travelBannerInfo.orEmpty(), @@ -318,6 +319,8 @@ data class HomeData( val showChatIcon: Boolean, val hasUnseenChatMessages: Boolean, val showHelpCenter: Boolean, + // Backed by the member's change-tier eligibility; gates the "Edit insurance" quick-action tile. + val isEditInsuranceEnabled: Boolean, val firstVetSections: List, val crossSells: CrossSellSheetData, val addonBannerInfos: List, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt index c445223692..0374fabf61 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt @@ -26,6 +26,7 @@ internal class GetHomeDataUseCaseDemo : GetHomeDataUseCase { ), hasUnseenChatMessages = false, showHelpCenter = true, + isEditInsuranceEnabled = true, firstVetSections = listOf(), crossSells = CrossSellSheetData( recommendedCrossSell = diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index d9b1879128..28bec4911d 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -901,6 +901,7 @@ private fun HomeScreenSuccess( ) HomeSection.QuickActionTiles -> QuickActionTilesSection( + isEditInsuranceEnabled = uiState.isEditInsuranceEnabled, onEditInsurance = navigateToEditInsurance, onChangeAddress = navigateToMovingFlow, onTravelCertificate = navigateToTravelCertificate, @@ -1161,6 +1162,7 @@ private fun OffersSection( @Composable private fun QuickActionTilesSection( + isEditInsuranceEnabled: Boolean, onEditInsurance: () -> Unit, onChangeAddress: () -> Unit, onTravelCertificate: () -> Unit, @@ -1184,14 +1186,16 @@ private fun QuickActionTilesSection( .fillMaxWidth() .height(IntrinsicSize.Max), ) { - HomeActionTile( - icon = HedvigIcons.Settings, - text = stringResource(Res.string.HOME_QUICK_ACTIONS_EDIT_INSURANCE), - onClick = onEditInsurance, - modifier = Modifier - .weight(1f) - .fillMaxHeight(), - ) + if (isEditInsuranceEnabled) { + HomeActionTile( + icon = HedvigIcons.Settings, + text = stringResource(Res.string.HOME_QUICK_ACTIONS_EDIT_INSURANCE), + onClick = onEditInsurance, + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + ) + } HomeActionTile( icon = HedvigIcons.Reload, text = stringResource(Res.string.HOME_QUICK_ACTIONS_CHANGE_ADDRESS), @@ -1431,6 +1435,7 @@ private fun PreviewHomeScreen( connectPayment = ConnectPayment(), ), isHelpCenterEnabled = true, + isEditInsuranceEnabled = true, hasUnseenChatMessages = hasUnseenChatMessages, crossSellsPartition = CrossSellsPartition( offersCrossSell = RecommendedCrossSell( @@ -1594,6 +1599,7 @@ private fun PreviewHomeScreenAllHomeTextTypes( updateContactInfo = null, ), isHelpCenterEnabled = false, + isEditInsuranceEnabled = true, hasUnseenChatMessages = false, crossSellsAction = null, firstVetAction = null, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt index d716be1460..2762f1bbd3 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt @@ -147,6 +147,7 @@ internal class HomePresenter( !alreadySeenImportantMessages.contains(it.id) }, isHelpCenterEnabled = successData.showHelpCenter, + isEditInsuranceEnabled = successData.isEditInsuranceEnabled, hasUnseenChatMessages = successData.hasUnseenChatMessages, chatAction = successData.chatAction, firstVetAction = successData.firstVetAction, @@ -196,6 +197,7 @@ internal sealed interface HomeUiState { val addonBannerInfos: List, val isProduction: Boolean, override val isHelpCenterEnabled: Boolean, + val isEditInsuranceEnabled: Boolean, override val hasUnseenChatMessages: Boolean, val crossSellsPartition: CrossSellsPartition = CrossSellsPartition(), val firstName: String = "", @@ -213,6 +215,7 @@ private data class SuccessData( val veryImportantMessages: List, val memberReminders: MemberReminders, val showHelpCenter: Boolean, + val isEditInsuranceEnabled: Boolean, val chatAction: HomeTopBarAction.ChatAction?, val firstVetAction: HomeTopBarAction.FirstVetAction?, val crossSellsAction: HomeTopBarAction.CrossSellsAction?, @@ -231,6 +234,7 @@ private data class SuccessData( veryImportantMessages = lastState.veryImportantMessages, memberReminders = lastState.memberReminders, showHelpCenter = lastState.isHelpCenterEnabled, + isEditInsuranceEnabled = lastState.isEditInsuranceEnabled, crossSellsAction = lastState.crossSellsAction, firstVetAction = lastState.firstVetAction, hasUnseenChatMessages = lastState.hasUnseenChatMessages, @@ -282,6 +286,7 @@ private data class SuccessData( enableNotifications = null, ), showHelpCenter = homeData.showHelpCenter, + isEditInsuranceEnabled = homeData.isEditInsuranceEnabled, firstVetAction = firstVetAction, crossSellsAction = crossSellsAction, hasUnseenChatMessages = homeData.hasUnseenChatMessages, diff --git a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt index 4c12dc5366..6a74552c9b 100644 --- a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt +++ b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt @@ -162,6 +162,7 @@ internal class HomePresenterTest { showChatIcon = true, hasUnseenChatMessages = false, showHelpCenter = false, + isEditInsuranceEnabled = false, crossSells = CrossSellSheetData(testCrossSell, listOf(), null), firstVetSections = listOf(), addonBannerInfos = emptyList(), @@ -187,6 +188,7 @@ internal class HomePresenterTest { veryImportantMessages = listOf(), memberReminders = MemberReminders(), isHelpCenterEnabled = false, + isEditInsuranceEnabled = false, firstVetAction = null, crossSellsAction = HomeTopBarAction.CrossSellsAction( CrossSellSheetData(testCrossSell, listOf(), null), @@ -231,6 +233,7 @@ internal class HomePresenterTest { crossSells = addonOnlyCrossSells, firstVetSections = listOf(), showHelpCenter = false, + isEditInsuranceEnabled = false, addonBannerInfos = listOf(), draftClaim = null, ).right(), @@ -275,6 +278,7 @@ internal class HomePresenterTest { crossSells = CrossSellSheetData(null, listOf(), null), firstVetSections = listOf(), showHelpCenter = false, + isEditInsuranceEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -289,6 +293,7 @@ internal class HomePresenterTest { connectPayment = null, ), isHelpCenterEnabled = false, + isEditInsuranceEnabled = false, hasUnseenChatMessages = false, chatAction = null, firstVetAction = null, @@ -352,6 +357,7 @@ internal class HomePresenterTest { showChatIcon = false, hasUnseenChatMessages = hasNotification, showHelpCenter = false, + isEditInsuranceEnabled = false, firstVetSections = listOf(), crossSells = CrossSellSheetData(null, listOf(), null), addonBannerInfos = emptyList(), @@ -391,6 +397,7 @@ internal class HomePresenterTest { crossSells = CrossSellSheetData(null, listOf(), null), firstVetSections = listOf(), showHelpCenter = false, + isEditInsuranceEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -403,6 +410,7 @@ internal class HomePresenterTest { veryImportantMessages = listOf(), memberReminders = MemberReminders(), isHelpCenterEnabled = false, + isEditInsuranceEnabled = false, hasUnseenChatMessages = false, chatAction = null, firstVetAction = null, @@ -448,6 +456,7 @@ internal class HomePresenterTest { firstVet, ), showHelpCenter = false, + isEditInsuranceEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -460,6 +469,7 @@ internal class HomePresenterTest { veryImportantMessages = listOf(), memberReminders = MemberReminders(), isHelpCenterEnabled = false, + isEditInsuranceEnabled = false, hasUnseenChatMessages = false, chatAction = null, firstVetAction = HomeTopBarAction.FirstVetAction(listOf(firstVet)), @@ -504,6 +514,7 @@ internal class HomePresenterTest { crossSells = CrossSellSheetData(testCrossSell, listOf(crossSell), null), firstVetSections = listOf(), showHelpCenter = false, + isEditInsuranceEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -516,6 +527,7 @@ internal class HomePresenterTest { veryImportantMessages = listOf(), memberReminders = MemberReminders(), isHelpCenterEnabled = false, + isEditInsuranceEnabled = false, hasUnseenChatMessages = false, chatAction = null, firstVetAction = null, @@ -561,6 +573,7 @@ internal class HomePresenterTest { crossSells = CrossSellSheetData(null, emptyList(), null), firstVetSections = listOf(), showHelpCenter = false, + isEditInsuranceEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -573,6 +586,7 @@ internal class HomePresenterTest { veryImportantMessages = listOf(), memberReminders = MemberReminders(), isHelpCenterEnabled = false, + isEditInsuranceEnabled = false, hasUnseenChatMessages = false, chatAction = HomeTopBarAction.ChatAction, firstVetAction = null, @@ -610,6 +624,7 @@ internal class HomePresenterTest { crossSells = CrossSellSheetData(null, emptyList(), null), firstVetSections = listOf(), showHelpCenter = false, + isEditInsuranceEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -622,6 +637,7 @@ internal class HomePresenterTest { veryImportantMessages = listOf(), memberReminders = MemberReminders(), isHelpCenterEnabled = false, + isEditInsuranceEnabled = false, hasUnseenChatMessages = false, chatAction = null, firstVetAction = null, @@ -769,6 +785,7 @@ internal class HomePresenterTest { showChatIcon = false, hasUnseenChatMessages = false, showHelpCenter = false, + isEditInsuranceEnabled = false, firstVetSections = listOf(), crossSells = CrossSellSheetData(null, emptyList(), null), addonBannerInfos = emptyList(), From d8c1c87f7ad9c3ace2a4ff1342a217ac557e6879 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 5 Aug 2026 14:05:36 +0200 Subject: [PATCH 13/23] cross-sells: honor section buttonText in the discounts path CrossSellsSection.buttonText was only applied on the non-discount branch; the discounts branch used the per-item crossSell.buttonText and silently ignored the section-level override. Fall back to the section value when the item has none (crossSell.buttonText ?: buttonText), so the parameter behaves consistently across both branches. No current caller combines a section buttonText with discounts, so existing output is unchanged. --- .../src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt b/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt index 0692785644..a8de3902bd 100644 --- a/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt +++ b/app/ui/cross-sells/src/main/kotlin/com/hedvig/android/crosssells/CrossSells.kt @@ -629,7 +629,7 @@ fun CrossSellsSection( imageLoader = imageLoader, crossSellImageAsset = crossSell.pillowImage, onSheetDismissed = onSheetDismissed, - buttonText = crossSell.buttonText, + buttonText = crossSell.buttonText ?: buttonText, buttonSize = buttonSize, buttonShape = buttonShape, ) From 4d2ea847d8097a6604091af7c7b0aa233c628da8 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 5 Aug 2026 15:30:40 +0200 Subject: [PATCH 14/23] feature-home: drop the vestigial isHelpCenterEnabled interface property isHelpCenterEnabled is only ever read off HomeUiState.Success, never polymorphically through the HomeUiState interface, so its interface declaration (with a get() = false default) plus the Success override were dead weight. Make it a plain Success val, leaving the interface to hold only the state genuinely read through it (isReloading, hasUnseenChatMessages). --- .../com/hedvig/android/feature/home/home/ui/HomePresenter.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt index 2762f1bbd3..e5689fff4d 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt @@ -179,9 +179,6 @@ internal sealed interface HomeUiState { val isReloading: Boolean get() = false - val isHelpCenterEnabled: Boolean - get() = false - val hasUnseenChatMessages: Boolean get() = false @@ -196,7 +193,7 @@ internal sealed interface HomeUiState { val crossSellsAction: HomeTopBarAction.CrossSellsAction?, val addonBannerInfos: List, val isProduction: Boolean, - override val isHelpCenterEnabled: Boolean, + val isHelpCenterEnabled: Boolean, val isEditInsuranceEnabled: Boolean, override val hasUnseenChatMessages: Boolean, val crossSellsPartition: CrossSellsPartition = CrossSellsPartition(), From d7dc3f4a4f219d43e85df5fa9da578f7805e9a93 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 5 Aug 2026 15:31:02 +0200 Subject: [PATCH 15/23] feature-home: gate all quick-action tiles on member eligibility Extend the eligibility gating from just "Edit insurance" to all three quick-action tiles, so an ineligible member never sees a tile that dead-ends. - Fetch isMovingEnabled and isTravelCertificateEnabled on the memberActions block QueryHome already requests, alongside isChangeTierEnabled. - Map them into HomeData and thread through the HomePresenter state. - Gate "Change address" and "Travel certificate" like "Edit insurance", and hide the whole quick-actions section when none of the three are enabled so it never renders as a lone heading over an empty row. - Cover the per-flag mapping with one focused HomePresenter test. --- .../src/main/graphql/QueryHome.graphql | 2 + .../home/home/data/GetHomeDataUseCase.kt | 8 ++- .../home/home/data/GetHomeDataUseCaseDemo.kt | 2 + .../feature/home/home/ui/HomeDestination.kt | 46 ++++++++------ .../feature/home/home/ui/HomePresenter.kt | 10 +++ .../feature/home/home/ui/HomePresenterTest.kt | 61 +++++++++++++++++++ 6 files changed, 111 insertions(+), 18 deletions(-) diff --git a/app/feature/feature-home/src/main/graphql/QueryHome.graphql b/app/feature/feature-home/src/main/graphql/QueryHome.graphql index 08b4754b9e..1ab4696adc 100644 --- a/app/feature/feature-home/src/main/graphql/QueryHome.graphql +++ b/app/feature/feature-home/src/main/graphql/QueryHome.graphql @@ -82,6 +82,8 @@ query Home($claimsHistoryFlag: Boolean!, $resumeClaimEnabled: Boolean!) { } memberActions { isChangeTierEnabled + isMovingEnabled + isTravelCertificateEnabled firstVetAction { sections { title diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt index d5921acdf3..c62821a01e 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt @@ -201,6 +201,9 @@ internal class GetHomeDataUseCaseImpl( hasUnseenChatMessages = hasUnseenChatMessages, showHelpCenter = true, isEditInsuranceEnabled = homeQueryData.currentMember.memberActions?.isChangeTierEnabled ?: false, + isMovingEnabled = homeQueryData.currentMember.memberActions?.isMovingEnabled ?: false, + isTravelCertificateEnabled = homeQueryData.currentMember.memberActions?.isTravelCertificateEnabled + ?: false, firstVetSections = firstVetActions, crossSells = crossSells, addonBannerInfos = travelBannerInfo.orEmpty(), @@ -319,8 +322,11 @@ data class HomeData( val showChatIcon: Boolean, val hasUnseenChatMessages: Boolean, val showHelpCenter: Boolean, - // Backed by the member's change-tier eligibility; gates the "Edit insurance" quick-action tile. + // Per-member eligibility for each quick-action tile; a tile is hidden when its flag is false, and the + // whole quick-actions section is hidden when none are enabled. val isEditInsuranceEnabled: Boolean, + val isMovingEnabled: Boolean, + val isTravelCertificateEnabled: Boolean, val firstVetSections: List, val crossSells: CrossSellSheetData, val addonBannerInfos: List, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt index 0374fabf61..96688273d3 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt @@ -27,6 +27,8 @@ internal class GetHomeDataUseCaseDemo : GetHomeDataUseCase { hasUnseenChatMessages = false, showHelpCenter = true, isEditInsuranceEnabled = true, + isMovingEnabled = true, + isTravelCertificateEnabled = true, firstVetSections = listOf(), crossSells = CrossSellSheetData( recommendedCrossSell = diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 28bec4911d..209ed42612 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -663,7 +663,7 @@ private fun HomeScreenSuccess( } HomeSection.QuickActionTiles -> { - true + uiState.isEditInsuranceEnabled || uiState.isMovingEnabled || uiState.isTravelCertificateEnabled } } } @@ -902,6 +902,8 @@ private fun HomeScreenSuccess( HomeSection.QuickActionTiles -> QuickActionTilesSection( isEditInsuranceEnabled = uiState.isEditInsuranceEnabled, + isMovingEnabled = uiState.isMovingEnabled, + isTravelCertificateEnabled = uiState.isTravelCertificateEnabled, onEditInsurance = navigateToEditInsurance, onChangeAddress = navigateToMovingFlow, onTravelCertificate = navigateToTravelCertificate, @@ -1163,6 +1165,8 @@ private fun OffersSection( @Composable private fun QuickActionTilesSection( isEditInsuranceEnabled: Boolean, + isMovingEnabled: Boolean, + isTravelCertificateEnabled: Boolean, onEditInsurance: () -> Unit, onChangeAddress: () -> Unit, onTravelCertificate: () -> Unit, @@ -1196,22 +1200,26 @@ private fun QuickActionTilesSection( .fillMaxHeight(), ) } - HomeActionTile( - icon = HedvigIcons.Reload, - text = stringResource(Res.string.HOME_QUICK_ACTIONS_CHANGE_ADDRESS), - onClick = onChangeAddress, - modifier = Modifier - .weight(1f) - .fillMaxHeight(), - ) - HomeActionTile( - icon = HedvigIcons.Travel, - text = stringResource(Res.string.HC_QUICK_ACTIONS_TRAVEL_CERTIFICATE), - onClick = onTravelCertificate, - modifier = Modifier - .weight(1f) - .fillMaxHeight(), - ) + if (isMovingEnabled) { + HomeActionTile( + icon = HedvigIcons.Reload, + text = stringResource(Res.string.HOME_QUICK_ACTIONS_CHANGE_ADDRESS), + onClick = onChangeAddress, + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + ) + } + if (isTravelCertificateEnabled) { + HomeActionTile( + icon = HedvigIcons.Travel, + text = stringResource(Res.string.HC_QUICK_ACTIONS_TRAVEL_CERTIFICATE), + onClick = onTravelCertificate, + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + ) + } } } } @@ -1436,6 +1444,8 @@ private fun PreviewHomeScreen( ), isHelpCenterEnabled = true, isEditInsuranceEnabled = true, + isMovingEnabled = true, + isTravelCertificateEnabled = true, hasUnseenChatMessages = hasUnseenChatMessages, crossSellsPartition = CrossSellsPartition( offersCrossSell = RecommendedCrossSell( @@ -1600,6 +1610,8 @@ private fun PreviewHomeScreenAllHomeTextTypes( ), isHelpCenterEnabled = false, isEditInsuranceEnabled = true, + isMovingEnabled = true, + isTravelCertificateEnabled = true, hasUnseenChatMessages = false, crossSellsAction = null, firstVetAction = null, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt index e5689fff4d..6653affff2 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt @@ -148,6 +148,8 @@ internal class HomePresenter( }, isHelpCenterEnabled = successData.showHelpCenter, isEditInsuranceEnabled = successData.isEditInsuranceEnabled, + isMovingEnabled = successData.isMovingEnabled, + isTravelCertificateEnabled = successData.isTravelCertificateEnabled, hasUnseenChatMessages = successData.hasUnseenChatMessages, chatAction = successData.chatAction, firstVetAction = successData.firstVetAction, @@ -195,6 +197,8 @@ internal sealed interface HomeUiState { val isProduction: Boolean, val isHelpCenterEnabled: Boolean, val isEditInsuranceEnabled: Boolean, + val isMovingEnabled: Boolean, + val isTravelCertificateEnabled: Boolean, override val hasUnseenChatMessages: Boolean, val crossSellsPartition: CrossSellsPartition = CrossSellsPartition(), val firstName: String = "", @@ -213,6 +217,8 @@ private data class SuccessData( val memberReminders: MemberReminders, val showHelpCenter: Boolean, val isEditInsuranceEnabled: Boolean, + val isMovingEnabled: Boolean, + val isTravelCertificateEnabled: Boolean, val chatAction: HomeTopBarAction.ChatAction?, val firstVetAction: HomeTopBarAction.FirstVetAction?, val crossSellsAction: HomeTopBarAction.CrossSellsAction?, @@ -232,6 +238,8 @@ private data class SuccessData( memberReminders = lastState.memberReminders, showHelpCenter = lastState.isHelpCenterEnabled, isEditInsuranceEnabled = lastState.isEditInsuranceEnabled, + isMovingEnabled = lastState.isMovingEnabled, + isTravelCertificateEnabled = lastState.isTravelCertificateEnabled, crossSellsAction = lastState.crossSellsAction, firstVetAction = lastState.firstVetAction, hasUnseenChatMessages = lastState.hasUnseenChatMessages, @@ -284,6 +292,8 @@ private data class SuccessData( ), showHelpCenter = homeData.showHelpCenter, isEditInsuranceEnabled = homeData.isEditInsuranceEnabled, + isMovingEnabled = homeData.isMovingEnabled, + isTravelCertificateEnabled = homeData.isTravelCertificateEnabled, firstVetAction = firstVetAction, crossSellsAction = crossSellsAction, hasUnseenChatMessages = homeData.hasUnseenChatMessages, diff --git a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt index 6a74552c9b..064279defb 100644 --- a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt +++ b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt @@ -163,6 +163,8 @@ internal class HomePresenterTest { hasUnseenChatMessages = false, showHelpCenter = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, crossSells = CrossSellSheetData(testCrossSell, listOf(), null), firstVetSections = listOf(), addonBannerInfos = emptyList(), @@ -189,6 +191,8 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), isHelpCenterEnabled = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, firstVetAction = null, crossSellsAction = HomeTopBarAction.CrossSellsAction( CrossSellSheetData(testCrossSell, listOf(), null), @@ -234,6 +238,8 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, addonBannerInfos = listOf(), draftClaim = null, ).right(), @@ -279,6 +285,8 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -294,6 +302,8 @@ internal class HomePresenterTest { ), isHelpCenterEnabled = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, hasUnseenChatMessages = false, chatAction = null, firstVetAction = null, @@ -358,6 +368,8 @@ internal class HomePresenterTest { hasUnseenChatMessages = hasNotification, showHelpCenter = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, firstVetSections = listOf(), crossSells = CrossSellSheetData(null, listOf(), null), addonBannerInfos = emptyList(), @@ -398,6 +410,8 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -411,6 +425,8 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), isHelpCenterEnabled = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, hasUnseenChatMessages = false, chatAction = null, firstVetAction = null, @@ -457,6 +473,8 @@ internal class HomePresenterTest { ), showHelpCenter = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -470,6 +488,8 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), isHelpCenterEnabled = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, hasUnseenChatMessages = false, chatAction = null, firstVetAction = HomeTopBarAction.FirstVetAction(listOf(firstVet)), @@ -515,6 +535,8 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -528,6 +550,8 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), isHelpCenterEnabled = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, hasUnseenChatMessages = false, chatAction = null, firstVetAction = null, @@ -574,6 +598,8 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -587,6 +613,8 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), isHelpCenterEnabled = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, hasUnseenChatMessages = false, chatAction = HomeTopBarAction.ChatAction, firstVetAction = null, @@ -625,6 +653,8 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -638,6 +668,8 @@ internal class HomePresenterTest { memberReminders = MemberReminders(), isHelpCenterEnabled = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, hasUnseenChatMessages = false, chatAction = null, firstVetAction = null, @@ -777,6 +809,33 @@ internal class HomePresenterTest { } } + @Test + fun `member action eligibility flags each propagate independently to the ui state`() = runTest { + val getHomeDataUseCase = TestGetHomeDataUseCase() + val homePresenter = HomePresenter( + getHomeDataUseCase, + SeenImportantMessagesStorageImpl(), + FakeCrossSellHomeNotificationService(), + ApplicationScope(backgroundScope), + false, + TestDeleteClaimIntentDraftUseCase(), + ) + homePresenter.test(HomeUiState.Loading) { + assertThat(awaitItem()).isInstanceOf() + getHomeDataUseCase.responseTurbine.add( + someIrrelevantHomeDataInstance.copy( + isEditInsuranceEnabled = true, + isMovingEnabled = false, + isTravelCertificateEnabled = true, + ).right(), + ) + val success = assertThat(awaitItem()).isInstanceOf() + success.prop(HomeUiState.Success::isEditInsuranceEnabled).isTrue() + success.prop(HomeUiState.Success::isMovingEnabled).isFalse() + success.prop(HomeUiState.Success::isTravelCertificateEnabled).isTrue() + } + } + private val someIrrelevantHomeDataInstance: HomeData = HomeData( contractStatus = HomeData.ContractStatus.Active, claimStatusCardsData = null, @@ -786,6 +845,8 @@ internal class HomePresenterTest { hasUnseenChatMessages = false, showHelpCenter = false, isEditInsuranceEnabled = false, + isMovingEnabled = false, + isTravelCertificateEnabled = false, firstVetSections = listOf(), crossSells = CrossSellSheetData(null, emptyList(), null), addonBannerInfos = emptyList(), From 63e20f87333f86d573c311c7c42880c9e45fd1ac Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 5 Aug 2026 15:58:41 +0200 Subject: [PATCH 16/23] docs: design for Home quick-action tiles mirroring the Help Center --- ...quick-actions-mirror-help-center-design.md | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-05-home-quick-actions-mirror-help-center-design.md diff --git a/docs/superpowers/specs/2026-08-05-home-quick-actions-mirror-help-center-design.md b/docs/superpowers/specs/2026-08-05-home-quick-actions-mirror-help-center-design.md new file mode 100644 index 0000000000..5d7bb8c9d3 --- /dev/null +++ b/docs/superpowers/specs/2026-08-05-home-quick-actions-mirror-help-center-design.md @@ -0,0 +1,130 @@ +# Home quick-action tiles mirror the Help Center + +## Goal + +The three quick-action tiles on the Home screen should stop being a hardcoded set +(Edit insurance / Change address / Travel certificate) and instead show the **first +three** of the member's actual Help Center quick-action list, in the same order, with +the same eligibility and the same behavior, each rendered with an appropriate icon. + +Specifically: + +- The "Edit insurance" tile must not navigate directly to the change-tier flow. It must + mirror the Help Center's "Edit insurance" option, which opens a bottom sheet of + sub-options (edit co-insured, edit co-owners, change coverage, terminate) filtered by + eligibility. +- Every tile taps through to exactly the destination the Help Center would use for that + action. + +## Background / current state + +The Help Center quick-action machinery lives entirely in `feature-help-center`: + +- `GetMemberActionsUseCase` runs `MemberActionsQuery` and returns a project-owned + `MemberAction` (the `is…Enabled` eligibility flags plus FirstVet/SickAbroad details). +- `GetQuickLinksUseCase` builds an ordered `List` from `MemberAction` (plus a + contracts query for co-insured/co-owner). Order: **Edit insurance → Change address → + Payments → Travel certificate → FirstVet → Sick abroad**, each present only if eligible. +- `QuickAction` is `StandaloneQuickLink(titleRes, hintTextRes, quickLinkDestination)` or + `MultiSelectExpandedLink(titleRes, hintTextRes, links)`. "Edit insurance" is the + multi-select; tapping it opens a bottom sheet listing its sub-links. +- `QuickLinkDestination` has `OuterDestination` (navigable flows: change tier, change + address, connect payment, travel certificate, termination, co-insured/co-owner) and + `InnerHelpCenterDestination` (FirstVet → the member's FirstVet sections; SickAbroad → + `EmergencyKey(deflectData)`; deflect UI already lives in the shared + `app/shared/partners-deflect` module). + +`feature-home` cannot depend on `feature-help-center` (feature-to-feature dependencies are +banned), so none of this is reachable from Home today. + +Home currently gates three hardcoded tiles on three booleans +(`isEditInsuranceEnabled` / `isMovingEnabled` / `isTravelCertificateEnabled`) sourced from +`memberActions` fields added to `QueryHome`. This design supersedes that (see +"Relationship to the recent gating work"). + +## Non-goals (YAGNI) + +- No icons added to the Help Center list; Help Center's appearance is unchanged. +- No generic cross-feature quick-actions **UI** component; Home renders its own tiles and + its own Edit-insurance sheet. +- No change to Help Center behavior, ordering, or destinations. + +## Architecture + +### 1. New shared module: `app/shared/member-quick-actions` + +A KMP module (commonMain, no UI, no design-system dependency) that becomes the single +source of truth for member actions and quick links. Moved into it from +`feature-help-center`: + +- `GetMemberActionsUseCase` + `MemberAction` (and `MemberActionWithDetails`). +- `GetQuickLinksUseCase`, renamed `GetMemberQuickActionsUseCase`, + `QuickLinkDestination`. +- `QuickAction` model. +- The GraphQL operations they own: `MemberActionsQuery`, `AvailableSelfServiceOnContractsQuery`. + +`QuickLinkDestination` (including the previously-`internal` `InnerHelpCenterDestination` +cases for FirstVet and SickAbroad) becomes public in the shared module, since both Help +Center and Home map destinations to navigation. + +Dependencies: apollo octopus, featureflags, `shared/partners-deflect` (for `DeflectData`), +`ui-emergency` (for `FirstVetSection`), core-common, logging, metro. Bindings remain +`@ContributesBinding(AppScope::class)`, so DI wiring is unchanged. + +### 2. Help Center refactor (pure move, no behavior change) + +`feature-help-center` depends on the new module and imports the moved types/use cases +instead of its local copies. List, ordering, and the Edit-insurance multi-select render +exactly as today. `GetQuickLinksUseCaseTest` moves to the shared module. + +### 3. Home consumption + +`HomePresenter` injects `GetMemberQuickActionsUseCase`, obtains `List`, takes +the **first three** (no exclusions — FirstVet and SickAbroad are both navigable from Home), +and exposes them on `HomeUiState.Success`. This replaces the three `is…Enabled` booleans +and the hardcoded tiles, and lets Home drop the `memberActions` fields from `QueryHome` +(the shared use case runs its own query). The quick-actions section is hidden when the list +is empty (same empty-guard behavior, now list-driven). + +### 4. Home UI + +Render up to three tiles, each an icon plus the action's `titleRes`. The icon is mapped in +Home's UI layer from the action type / destination (e.g. `Settings` → Edit insurance, +`Reload` → Change address, a card icon → Payments, `Travel` → Travel certificate, a FirstVet +glyph → FirstVet, a fitting glyph → Sick abroad). Tap behavior: + +- `StandaloneQuickLink` → navigate via a Home `(QuickLinkDestination) -> Unit` handler. +- `MultiSelectExpandedLink` (Edit insurance) → open a Home-owned `HedvigBottomSheet` listing + its sub-links; each row navigates through the same handler. (The Help Center sheet + composable is not reusable cross-feature, so Home builds its own simple sheet.) + +### 5. Navigation wiring + +`:app` owns a single `navigateToQuickLink(destination: QuickLinkDestination)` mapping +(destination → `backstack.add(key)` / tab switch) and uses it to wire **both** Help Center +and Home, so the destination→nav-key mapping is not duplicated. SickAbroad → `EmergencyKey`, +FirstVet → Home's existing FirstVet destination, the rest to their existing keys (all in +`-navigation` modules `:app` already depends on). + +## Relationship to the recent gating work + +Two commits on this branch (`gate Edit insurance tile on change-tier eligibility` and +`gate all quick-action tiles on member eligibility`) implemented the interim hardcoded-tile +gating via boolean flags on `QueryHome`. This design replaces that implementation: the +booleans and the `QueryHome.memberActions` additions are removed in favor of the shared +`GetMemberQuickActionsUseCase`. The interface-refactor commit +(`drop the vestigial isHelpCenterEnabled interface property`) stands on its own and is kept. + +## Testing + +- Move `GetQuickLinksUseCaseTest` to the shared module. +- Add a `HomePresenter` test for the list mapping: first-three selection, fewer-than-three, + and the empty case (section hidden). +- Existing Help Center tests continue to pass unchanged after the move. + +## Risks / notes + +- The move touches `feature-help-center` broadly (imports); the plan should do the move as + a mechanical first step and verify Help Center compiles + tests pass before Home consumes + the module. +- `QuickLinkDestination` gains public visibility for its inner cases; confirm nothing relied + on their `internal` scoping for correctness (they are data holders). From f5e6dd575e2dd35808013c1489727f7b58153a44 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 5 Aug 2026 16:04:48 +0200 Subject: [PATCH 17/23] docs: implementation plan for Home quick-actions mirroring Help Center --- ...5-home-quick-actions-mirror-help-center.md | 560 ++++++++++++++++++ 1 file changed, 560 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-05-home-quick-actions-mirror-help-center.md diff --git a/docs/superpowers/plans/2026-08-05-home-quick-actions-mirror-help-center.md b/docs/superpowers/plans/2026-08-05-home-quick-actions-mirror-help-center.md new file mode 100644 index 0000000000..aa86e9f193 --- /dev/null +++ b/docs/superpowers/plans/2026-08-05-home-quick-actions-mirror-help-center.md @@ -0,0 +1,560 @@ +# Home Quick-Action Tiles Mirror the Help Center — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace Home's hardcoded quick-action tiles with the first three of the member's actual Help Center quick-action list (same items, order, eligibility, and behavior, including the Edit-insurance sub-options sheet). + +**Architecture:** Extract the member-actions / quick-links domain logic out of `feature-help-center` into a new KMP shared module `member-quick-actions` that both features depend on. The `QuickLinkDestination → HedvigNavKey` mapping moves into that shared module as `QuickLinkDestination.toNavKey()`, so both consumers just call it and `backstack.add(...)`. Home's presenter consumes the shared use case; Home's UI renders the first three actions as tiles. + +**Tech Stack:** Kotlin Multiplatform, Jetpack Compose, Apollo GraphQL (octopus), Metro DI, Molecule, Arrow, assertK + Turbine (tests). + +## Global Constraints + +- DI is Metro. Bindings are `@ContributesBinding(AppScope::class)` / `@SingleIn(AppScope::class)` + `@Inject`; never Koin, never `get()`. +- Navigation is Navigation 3. Destinations are `@Serializable HedvigNavKey`s; push with `backstack.add(...)`. No `NavController`, no route strings. +- Feature modules cannot depend on other feature modules; the shared module and `-navigation` modules are the carve-outs. +- Never expose `octopus.*` Apollo types in public API; confine them to `internal` impl classes and map to project-owned types. +- Never add strings to `strings.xml` (Lokalise-managed). Reuse existing `HC_QUICK_ACTIONS_*` / `HOME_*` keys; if a needed string is missing, hardcode English + `// TODO: Add "" / "" to Lokalise`. +- No em-dash in prose/comments. Run `./gradlew ktlintFormat` before each commit. +- Comments describe current code only (no history/migration/process notes). + +--- + +## File Structure + +**New shared module `app/shared/member-quick-actions`** (KMP, commonMain; project accessor `projects.memberQuickActions`): +- `build.gradle.kts` — KMP + apollo("octopus"). +- `src/commonMain/graphql/QueryMemberActions.graphql` — moved. +- `src/commonMain/graphql/QueryAvailableSelfServiceOnContracts.graphql` — moved. +- `.../data/GetMemberActionsUseCase.kt` — moved (`MemberAction`, `MemberActionWithDetails`, use case). +- `.../data/GetMemberQuickActionsUseCase.kt` — moved+renamed from `GetQuickLinksUseCase.kt` (`QuickLinkDestination`, the builder). +- `.../model/QuickAction.kt` — moved. +- `.../navigation/QuickLinkNavKey.kt` — new: `fun QuickLinkDestination.toNavKey(): HedvigNavKey` (mapping moved out of `HelpCenterPresenter`). +- `src/jvmTest/.../GetMemberQuickActionsUseCaseTest.kt` — moved from `GetQuickLinksUseCaseTest.kt`. + +**`feature-help-center`** — consumes the shared module; deletes the moved files; `HelpCenterPresenter` replaces its `when` mapping with `destination.toNavKey()`. + +**`feature-home`**: +- `QueryHome.graphql` — remove the `memberActions { isChangeTierEnabled isMovingEnabled isTravelCertificateEnabled }` additions. +- `GetHomeDataUseCase.kt` / `GetHomeDataUseCaseDemo.kt` — remove `isEditInsuranceEnabled` / `isMovingEnabled` / `isTravelCertificateEnabled` from `HomeData`. +- `HomePresenter.kt` — inject `GetMemberQuickActionsUseCase`; replace the three booleans on `HomeUiState.Success` with `quickActions: List` (first three). +- `HomeDestination.kt` — `QuickActionTilesSection` renders tiles from `quickActions`; new `EditInsuranceQuickActionSheet`; icon mapping. +- `HomePresenterTest.kt` — update construction sites; add first-3/fewer/empty test. + +**`:app`** — `HedvigEntryProvider.kt` passes a `navigateToQuickLink: (QuickLinkDestination) -> Unit` into `homeEntries(...)` implemented as `{ backstack.add(it.toNavKey()) }`. + +--- + +## Task 1: Create the shared module skeleton and move the GraphQL queries + +**Files:** +- Create: `app/shared/member-quick-actions/build.gradle.kts` +- Move: `app/feature/feature-help-center/src/commonMain/graphql/QueryMemberActions.graphql` → `app/shared/member-quick-actions/src/commonMain/graphql/QueryMemberActions.graphql` +- Move: `app/feature/feature-help-center/src/commonMain/graphql/QueryAvailableSelfServiceOnContracts.graphql` → `app/shared/member-quick-actions/src/commonMain/graphql/QueryAvailableSelfServiceOnContracts.graphql` + +**Interfaces:** +- Produces: a buildable module `projects.memberQuickActions` generating `octopus.MemberActionsQuery` and `octopus.AvailableSelfServiceOnContractsQuery`. + +- [ ] **Step 1: Create the module build file** + +Create `app/shared/member-quick-actions/build.gradle.kts`: + +```kotlin +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi + +plugins { + id("hedvig.multiplatform.library") + id("hedvig.multiplatform.library.android") + id("hedvig.gradle.plugin") +} + +hedvig { + apollo("octopus") + serialization() +} + +kotlin { + sourceSets { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + applyDefaultHierarchyTemplate { + common { + group("jvmAndAndroid") { + withAndroidLibraryTarget() + withJvm() + } + } + } + commonMain.dependencies { + implementation(libs.apollo.runtime) + implementation(libs.arrow.core) + implementation(libs.arrow.fx) + implementation(libs.coroutines.core) + implementation(libs.kotlinx.serialization.core) + implementation(projects.apolloCore) + implementation(projects.apolloOctopusPublic) + implementation(projects.coreCommonPublic) + implementation(projects.coreResources) + implementation(projects.dataCoinsured) + implementation(projects.featureFlags) + implementation(projects.loggingPublic) + implementation(projects.navigationCommon) + implementation(projects.partnersDeflect) + implementation(projects.uiEmergency) + // -navigation modules for QuickLinkDestination.toNavKey() (Task 3) + implementation(projects.featureChooseTierNavigation) + implementation(projects.featureConnectPaymentTrustlyNavigation) + implementation(projects.featureEditCoinsuredNavigation) + implementation(projects.featureMovingflowNavigation) + implementation(projects.featureTerminateInsuranceNavigation) + implementation(projects.featureTravelCertificateNavigation) + } + jvmTest.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.loggingTest) + } + } +} +``` + +Note: `EmergencyKey` and `FirstVetKey` are resolved in Task 3; if they live in modules not listed above, add those `-navigation` deps when Task 3's compile fails (see Task 3 Step 4). + +- [ ] **Step 2: Move the two GraphQL files** + +```bash +cd /Users/stylianosgakis/hedvig/apps/android_copy +mkdir -p app/shared/member-quick-actions/src/commonMain/graphql +git mv app/feature/feature-help-center/src/commonMain/graphql/QueryMemberActions.graphql \ + app/shared/member-quick-actions/src/commonMain/graphql/QueryMemberActions.graphql +git mv app/feature/feature-help-center/src/commonMain/graphql/QueryAvailableSelfServiceOnContracts.graphql \ + app/shared/member-quick-actions/src/commonMain/graphql/QueryAvailableSelfServiceOnContracts.graphql +``` + +- [ ] **Step 3: Confirm module auto-discovery and generate sources** + +Run: `./gradlew :member-quick-actions:generateApolloSources` +Expected: BUILD SUCCESSFUL, and `octopus.MemberActionsQuery` / `octopus.AvailableSelfServiceOnContractsQuery` are generated under the module's build dir. (settings.gradle.kts auto-includes any dir under `app/` with a `build.gradle.kts`.) + +- [ ] **Step 4: Commit** + +```bash +./gradlew ktlintFormat -q +git add -A +git commit -m "member-quick-actions: scaffold shared module and move member-actions graphql" +``` + +--- + +## Task 2: Move MemberAction model + GetMemberActionsUseCase into the shared module + +**Files:** +- Move: `app/feature/feature-help-center/.../data/MemberAction.kt` → `app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/MemberAction.kt` + +**Interfaces:** +- Produces: `com.hedvig.android.memberquickactions.GetMemberActionsUseCase` (public), returning `Either`; public `MemberAction`, `MemberActionWithDetails`. + +- [ ] **Step 1: Move the file and repackage** + +```bash +mkdir -p app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions +git mv app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/MemberAction.kt \ + app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/MemberAction.kt +``` + +- [ ] **Step 2: Move the use-case impl** + +The impl is `GetMemberActionsUseCase.kt` in help-center `data/`. Move it too: + +```bash +git mv app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt \ + app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/GetMemberActionsUseCaseImpl.kt +``` +(If `MemberAction.kt` and the use case are the same file already, skip the redundant move; keep one file.) + +- [ ] **Step 3: Repackage the moved files** + +In both moved files, change the package line to `package com.hedvig.android.memberquickactions` and change `internal` to public on `GetMemberActionsUseCase`, `MemberAction`, and `MemberActionWithDetails` (they are consumed cross-module now). Keep `GetMemberActionsUseCaseImpl` `internal` with its `@ContributesBinding(AppScope::class) @SingleIn(AppScope::class) @Inject`. Imports for `octopus.MemberActionsQuery`, `DeflectData`, `FirstVetSection` stay; they resolve from this module's deps. + +- [ ] **Step 4: Verify the shared module compiles** + +Run: `./gradlew :member-quick-actions:compileDebugKotlin` +Expected: BUILD SUCCESSFUL. (feature-help-center will NOT compile yet — fixed in Task 4.) + +- [ ] **Step 5: Commit** + +```bash +./gradlew ktlintFormat -q +git add -A +git commit -m "member-quick-actions: move MemberAction + GetMemberActionsUseCase into shared module" +``` + +--- + +## Task 3: Move QuickAction / QuickLinkDestination / the builder, and add toNavKey() + +**Files:** +- Move: `.../model/QuickAction.kt` → `app/shared/member-quick-actions/.../QuickAction.kt` +- Move+rename: `.../data/GetQuickLinksUseCase.kt` → `app/shared/member-quick-actions/.../GetMemberQuickActionsUseCase.kt` +- Create: `app/shared/member-quick-actions/.../QuickLinkNavKey.kt` + +**Interfaces:** +- Consumes: `GetMemberActionsUseCase` (Task 2). +- Produces: + - `sealed interface QuickAction { StandaloneQuickLink(titleRes, hintTextRes, quickLinkDestination); MultiSelectExpandedLink(titleRes, hintTextRes, links: List) }` (public) + - `sealed interface QuickLinkDestination` with public `OuterDestination` and public `InnerHelpCenterDestination` cases (rename the type out of the `internal` scoping; make inner cases public). + - `interface GetMemberQuickActionsUseCase { suspend fun invoke(): Either> }` (public). + - `fun QuickLinkDestination.toNavKey(): HedvigNavKey` (public). + +- [ ] **Step 1: Move + repackage QuickAction and the builder** + +```bash +git mv app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/model/QuickAction.kt \ + app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/QuickAction.kt +git mv app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetQuickLinksUseCase.kt \ + app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/GetMemberQuickActionsUseCase.kt +``` + +Repackage both to `package com.hedvig.android.memberquickactions`. In the builder file: +- Rename interface `GetQuickLinksUseCase` → `GetMemberQuickActionsUseCase` and impl `GetQuickLinksUseCaseImpl` → `GetMemberQuickActionsUseCaseImpl` (keep annotations). +- Make `QuickLinkDestination` and ALL its nested `OuterDestination` / `InnerHelpCenterDestination` cases public (remove `internal`); make `QuickAction` public. +- Keep `octopus.AvailableSelfServiceOnContractsQuery` import (resolves from this module). + +- [ ] **Step 2: Add the toNavKey() mapping** + +Create `app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/QuickLinkNavKey.kt`. Move the exact `when` block from `HelpCenterPresenter` (the `NavigateToQuickAction` handler, cases listed in the design). Content: + +```kotlin +package com.hedvig.android.memberquickactions + +import com.hedvig.android.data.coinsured.CoInsuredFlowType +import com.hedvig.android.feature.change.tier.navigation.StartTierFlowChooseInsuranceKey +import com.hedvig.android.feature.connect.payment.trustly.ui.TrustlyKey +import com.hedvig.android.feature.editcoinsured.navigation.CoInsuredAddInfoKey +import com.hedvig.android.feature.editcoinsured.navigation.CoInsuredAddOrRemoveKey +import com.hedvig.android.feature.editcoinsured.navigation.EditCoInsuredTriageKey +import com.hedvig.android.feature.movingflow.navigation.SelectContractForMovingKey +import com.hedvig.android.feature.movingflow.navigation.MovingSource +import com.hedvig.android.feature.terminateinsurance.navigation.TerminateInsuranceKey +import com.hedvig.android.feature.travelcertificate.navigation.TravelCertificateKey +import com.hedvig.android.memberquickactions.QuickLinkDestination.InnerHelpCenterDestination +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.ChooseInsuranceForEditCoInsured +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.ChooseInsuranceForEditCoOwners +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkChangeAddress +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkChangeTier +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkCoInsuredAddInfo +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkCoInsuredAddOrRemove +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkCoOwnerAddInfo +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkCoOwnerAddOrRemove +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkConnectPayment +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkTermination +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkTravelCertificate +import com.hedvig.android.navigation.common.HedvigNavKey +import com.hedvig.android.navigation.emergency.EmergencyKey + +fun QuickLinkDestination.toNavKey(): HedvigNavKey = when (this) { + is InnerHelpCenterDestination.FirstVet -> FirstVetNavKeyPlaceholder(sections) + is InnerHelpCenterDestination.QuickLinkSickAbroad -> EmergencyKey(deflectData) + QuickLinkChangeAddress -> SelectContractForMovingKey(MovingSource.OTHER) + is QuickLinkCoInsuredAddInfo -> CoInsuredAddInfoKey(contractId, CoInsuredFlowType.CoInsured) + is QuickLinkCoInsuredAddOrRemove -> CoInsuredAddOrRemoveKey(contractId, CoInsuredFlowType.CoInsured) + is QuickLinkCoOwnerAddInfo -> CoInsuredAddInfoKey(contractId, CoInsuredFlowType.CoOwners) + is QuickLinkCoOwnerAddOrRemove -> CoInsuredAddOrRemoveKey(contractId, CoInsuredFlowType.CoOwners) + QuickLinkConnectPayment -> TrustlyKey + QuickLinkTermination -> TerminateInsuranceKey(null) + QuickLinkTravelCertificate -> TravelCertificateKey + QuickLinkChangeTier -> StartTierFlowChooseInsuranceKey + ChooseInsuranceForEditCoInsured -> EditCoInsuredTriageKey() + ChooseInsuranceForEditCoOwners -> EditCoInsuredTriageKey(type = CoInsuredFlowType.CoOwners) +} +``` + +NOTE for the implementer: `FirstVetKey` and the exact import paths for `EmergencyKey`, `SelectContractForMovingKey`, `MovingSource`, and `TrustlyKey` must be taken from the real imports currently in `HelpCenterPresenter.kt` (open it and copy them verbatim; replace `FirstVetNavKeyPlaceholder` with the real `FirstVetKey(...)` used there). If `FirstVetKey` lives in `feature-home` (not a `-navigation` module), the FirstVet case cannot resolve here — in that case leave `InnerHelpCenterDestination.FirstVet` OUT of `toNavKey()` (throw `error("handled by caller")`) and let each caller map FirstVet itself (Help Center already has `FirstVetKey`; Home has its own FirstVet destination). Prefer this caller-handles-FirstVet approach if FirstVetKey is not in a shared/-navigation module. + +- [ ] **Step 3: Add any missing -navigation dependencies** + +Run: `./gradlew :member-quick-actions:compileDebugKotlin` +If unresolved-reference errors name a nav key, add the matching `projects.feature…Navigation` (or `projects.navigationEmergency`) to `commonMain.dependencies` in the module build file, then re-run until BUILD SUCCESSFUL. + +- [ ] **Step 4: Commit** + +```bash +./gradlew ktlintFormat -q +git add -A +git commit -m "member-quick-actions: move QuickAction/QuickLinkDestination + builder + toNavKey into shared module" +``` + +--- + +## Task 4: Refactor feature-help-center to consume the shared module + +**Files:** +- Modify: `app/feature/feature-help-center/build.gradle.kts` (add `implementation(projects.memberQuickActions)`) +- Modify: `HelpCenterPresenter.kt` (replace the `when` mapping with `destination.toNavKey()`; update imports) +- Modify: every help-center file importing the moved types (update imports to `com.hedvig.android.memberquickactions.*`) +- Move: `GetQuickLinksUseCaseTest.kt` → shared module `src/jvmTest/...` as `GetMemberQuickActionsUseCaseTest.kt` + +**Interfaces:** +- Consumes: `GetMemberQuickActionsUseCase`, `QuickAction`, `QuickLinkDestination`, `toNavKey()` from `projects.memberQuickActions`. + +- [ ] **Step 1: Add the dependency** + +In `feature-help-center/build.gradle.kts` `commonMain.dependencies`, add `implementation(projects.memberQuickActions)`. + +- [ ] **Step 2: Fix imports across help-center** + +Run to find broken references, then update each import from the old packages (`...help.center.data.QuickLinkDestination`, `...help.center.model.QuickAction`, `...help.center.data.GetQuickLinksUseCase`, `...help.center.data.GetMemberActionsUseCase`, `...help.center.data.MemberAction`) to `com.hedvig.android.memberquickactions.*`. Rename `GetQuickLinksUseCase` references to `GetMemberQuickActionsUseCase`: + +```bash +grep -rln "GetQuickLinksUseCase\|help.center.data.QuickLinkDestination\|help.center.model.QuickAction\|help.center.data.MemberAction\|help.center.data.GetMemberActionsUseCase" app/feature/feature-help-center/src +``` + +- [ ] **Step 3: Replace the mapping in HelpCenterPresenter** + +In the `NavigateToQuickAction` handler, delete the whole `when (val destination = event.destination) { … }` block that built `key` and replace with: + +```kotlin +is NavigateToQuickAction -> { + selectedQuickAction = null + backstack.add(event.destination.toNavKey()) +} +``` +(If Task 3 left FirstVet out of `toNavKey()`, keep only the FirstVet branch here: `val key = if (dest is InnerHelpCenterDestination.FirstVet) FirstVetKey(dest.sections) else dest.toNavKey()`.) Remove now-unused nav-key imports from `HelpCenterPresenter.kt`. + +- [ ] **Step 4: Move the test** + +```bash +git mv app/feature/feature-help-center/src/jvmTest/kotlin/GetQuickLinksUseCaseTest.kt \ + app/shared/member-quick-actions/src/jvmTest/kotlin/GetMemberQuickActionsUseCaseTest.kt +``` +Repackage/rename inside: update class name to `GetMemberQuickActionsUseCaseTest`, update imports to `com.hedvig.android.memberquickactions.*`, rename the use-case references. Keep the test bodies identical. + +- [ ] **Step 5: Verify help-center + shared tests pass** + +Run: `./gradlew :feature-help-center:compileDebugKotlin :member-quick-actions:jvmTest :feature-help-center:compileReleaseKotlin` +Expected: BUILD SUCCESSFUL; the moved test passes unchanged (proving the move preserved behavior). + +- [ ] **Step 6: Commit** + +```bash +./gradlew ktlintFormat -q +git add -A +git commit -m "feature-help-center: consume member-quick-actions shared module" +``` + +--- + +## Task 5: Home presenter consumes the shared use case; revert the interim gating fields + +**Files:** +- Modify: `feature-home/build.gradle.kts` (add `implementation(projects.memberQuickActions)`) +- Modify: `app/feature/feature-home/src/main/graphql/QueryHome.graphql` (remove the three `memberActions` fields) +- Modify: `GetHomeDataUseCase.kt` / `GetHomeDataUseCaseDemo.kt` (remove the three `is…Enabled` fields from `HomeData`) +- Modify: `HomePresenter.kt` (inject `GetMemberQuickActionsUseCase`; expose `quickActions: List` on `Success`) +- Modify: `HomePresenterTest.kt` + +**Interfaces:** +- Consumes: `GetMemberQuickActionsUseCase.invoke(): Either>`, `QuickAction`. +- Produces: `HomeUiState.Success.quickActions: List` (the first three of the member's list; empty on error). + +- [ ] **Step 1: Revert the QueryHome memberActions additions** + +In `QueryHome.graphql`, delete the `isChangeTierEnabled`, `isMovingEnabled`, `isTravelCertificateEnabled` lines added to the `memberActions { }` block (leave `firstVetAction { … }`). Run `./gradlew :feature-home:generateApolloSources` to confirm it still generates. + +- [ ] **Step 2: Remove the interim HomeData flags** + +In `GetHomeDataUseCase.kt` remove `isEditInsuranceEnabled` / `isMovingEnabled` / `isTravelCertificateEnabled` from the `HomeData` data class, the mapping in `GetHomeDataUseCaseImpl`, and the comment. In `GetHomeDataUseCaseDemo.kt` remove the three assignments. + +- [ ] **Step 3: Write the failing presenter test** + +Add `projects.memberQuickActions` to `feature-home/build.gradle.kts` (`commonMain` if KMP test needs it; the module is android — add to the main `dependencies { }` block matching how feature-home declares deps) and `projects.memberQuickActionsTest` is not needed (use a fake). In `HomePresenterTest.kt` add: + +```kotlin +@Test +fun `home shows the first three member quick actions`() = runTest { + val getHomeDataUseCase = TestGetHomeDataUseCase() + val quickActions = listOf( + editInsuranceMultiSelect, changeAddressLink, paymentsLink, travelCertificateLink, + ) + val homePresenter = HomePresenter( + getHomeDataUseCase, + SeenImportantMessagesStorageImpl(), + FakeCrossSellHomeNotificationService(), + ApplicationScope(backgroundScope), + false, + TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(quickActions.right()), + ) + homePresenter.test(HomeUiState.Loading) { + assertThat(awaitItem()).isInstanceOf() + getHomeDataUseCase.responseTurbine.add(someIrrelevantHomeDataInstance.right()) + val success = assertThat(awaitItem()).isInstanceOf() + success.prop(HomeUiState.Success::quickActions) + .isEqualTo(listOf(editInsuranceMultiSelect, changeAddressLink, paymentsLink)) + } +} +``` + +Define `FakeGetMemberQuickActionsUseCase(private val result: Either>)` implementing the interface returning `result`, and the four `QuickAction` fixtures (use `StandaloneQuickLink(titleRes = Res.string.HC_QUICK_ACTIONS_CHANGE_ADDRESS_TITLE, hintTextRes = …, quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkChangeAddress)` etc., and one `MultiSelectExpandedLink`). + +- [ ] **Step 4: Run it to confirm it fails** + +Run: `./gradlew :feature-home:testDebugUnitTest --tests "*HomePresenterTest*first three*"` +Expected: FAIL (compilation error — `quickActions` not on `Success` / constructor lacks the use case). + +- [ ] **Step 5: Implement the presenter change** + +In `HomePresenter.kt`: +- Add constructor param `private val getMemberQuickActionsUseCase: GetMemberQuickActionsUseCase`. +- Add `val quickActions: List` to `HomeUiState.Success`, to `SuccessData`, to `fromLastState` (from `lastState.quickActions`), and to the `present()` build (`quickActions = successData.quickActions`). +- In the data-load path (where `getHomeDataUseCase.invoke(...)` result maps to `SuccessData.fromHomeData(...)`), also fetch quick actions: `val quickActions = getMemberQuickActionsUseCase.invoke().getOrElse { emptyList() }.take(3)` and pass it into `fromHomeData(...)` (add a `quickActions` param) or set on `SuccessData`. Keep the fetch failure-tolerant (empty list → section hidden). + +- [ ] **Step 6: Run the test to confirm it passes** + +Run: `./gradlew :feature-home:testDebugUnitTest --tests "*HomePresenterTest*"` +Expected: PASS (all existing tests still pass after adding the new constructor arg and `quickActions` to their expected `Success` — set `quickActions = emptyList()` on existing expected `Success` instances and pass `FakeGetMemberQuickActionsUseCase(emptyList().right())` to their presenter construction). + +- [ ] **Step 7: Commit** + +```bash +./gradlew ktlintFormat -q +git add -A +git commit -m "feature-home: source quick actions from the shared use case; drop interim eligibility flags" +``` + +--- + +## Task 6: Home UI renders the tiles and the Edit-insurance sheet + +**Files:** +- Modify: `HomeDestination.kt` (`QuickActionTilesSection`, icon mapping, `EditInsuranceQuickActionSheet`, section visibility, previews) +- Modify: `HomeEntries.kt` (thread `navigateToQuickLink`) +- Modify: `HedvigEntryProvider.kt` (`navigateToQuickLink = { backstack.add(it.toNavKey()) }`) + +**Interfaces:** +- Consumes: `Success.quickActions`, `QuickAction`, `QuickLinkDestination`, `toNavKey()`. +- Produces: tiles that navigate; Edit-insurance sheet. + +- [ ] **Step 1: Thread the navigation lambda** + +In `HomeEntries.kt` add param `navigateToQuickLink: (QuickLinkDestination) -> Unit` and pass it to `HomeDestination`. In `HedvigEntryProvider.kt`'s `homeEntries(...)` call add `navigateToQuickLink = { backstack.add(it.toNavKey()) }` (import `com.hedvig.android.memberquickactions.toNavKey`; add `implementation(projects.memberQuickActions)` to `app/app/build.gradle.kts` if not transitively present). If FirstVet is caller-handled (Task 3), map it here: `navigateToQuickLink = { d -> if (d is InnerHelpCenterDestination.FirstVet) backstack.add(FirstVetKey(d.sections)) else backstack.add(d.toNavKey()) }`. + +- [ ] **Step 2: Replace QuickActionTilesSection** + +Rewrite `QuickActionTilesSection` to take `quickActions: List`, `onQuickLink: (QuickLinkDestination) -> Unit`, and an `onEditInsurance: (MultiSelectExpandedLink) -> Unit`, rendering one `HomeActionTile` per action: + +```kotlin +@Composable +private fun QuickActionTilesSection( + quickActions: List, + onQuickLink: (QuickLinkDestination) -> Unit, + onEditInsurance: (QuickAction.MultiSelectExpandedLink) -> Unit, + horizontalInsets: PaddingValues, +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).padding(horizontalInsets), + ) { + HedvigText( + text = stringResource(Res.string.HC_QUICK_ACTIONS_TITLE), + style = HedvigTheme.typography.headlineSmall, + modifier = Modifier.semantics { heading() }, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Max), + ) { + quickActions.forEach { action -> + HomeActionTile( + icon = action.homeIcon(), + text = stringResource(action.titleRes), + onClick = { + when (action) { + is QuickAction.StandaloneQuickLink -> onQuickLink(action.quickLinkDestination) + is QuickAction.MultiSelectExpandedLink -> onEditInsurance(action) + } + }, + modifier = Modifier.weight(1f).fillMaxHeight(), + ) + } + } + } +} +``` + +- [ ] **Step 3: Add the icon mapping** + +Add a private helper mapping each action to a `HedvigIcons` glyph. Multi-select (Edit insurance) → `Settings`; standalone by destination type: + +```kotlin +@Composable +private fun QuickAction.homeIcon(): ImageVector = when (this) { + is QuickAction.MultiSelectExpandedLink -> HedvigIcons.Settings + is QuickAction.StandaloneQuickLink -> when (quickLinkDestination) { + QuickLinkDestination.OuterDestination.QuickLinkChangeAddress -> HedvigIcons.Reload + QuickLinkDestination.OuterDestination.QuickLinkConnectPayment -> HedvigIcons.Card + QuickLinkDestination.OuterDestination.QuickLinkTravelCertificate -> HedvigIcons.Travel + is QuickLinkDestination.InnerHelpCenterDestination.FirstVet -> HedvigIcons.HelipadOutline + is QuickLinkDestination.InnerHelpCenterDestination.QuickLinkSickAbroad -> HedvigIcons.HelipadOutline + else -> HedvigIcons.Settings + } +} +``` +(Verify each icon exists under `design-system-hedvig/.../icon/`; `Card` and `HelipadOutline` do. Swap to a closer glyph if design differs; these are sensible defaults per the spec.) + +- [ ] **Step 4: Add the Edit-insurance bottom sheet** + +Add a `HedvigBottomSheetState` in `HomeScreen` (like the existing `crossSellBottomSheetState`), shown via `onEditInsurance = { editInsuranceSheetState.show(it) }`, rendering the sub-links as rows that call `onQuickLink(link.quickLinkDestination)` then dismiss. Follow the existing `CrossSellBottomSheet` pattern in this file for structure (a `HedvigBottomSheet` with a `Column` of clickable rows using `link.titleRes`). + +- [ ] **Step 5: Update the section visibility + call site** + +In the `visibleSections` filter, change `HomeSection.QuickActionTiles ->` to `uiState.quickActions.isNotEmpty()`. Update the `QuickActionTilesSection(...)` call in `HomeScreenSuccess` to pass `quickActions = uiState.quickActions`, `onQuickLink = navigateToQuickLink`, `onEditInsurance = { editInsuranceSheetState.show(it) }`. Remove the old `isEditInsuranceEnabled` / `isMovingEnabled` / `isTravelCertificateEnabled` params and the `onEditInsurance/onChangeAddress/onTravelCertificate` lambdas. Delete now-unused imports (`HOME_QUICK_ACTIONS_EDIT_INSURANCE`, `HOME_QUICK_ACTIONS_CHANGE_ADDRESS`, `HedvigIcons.Reload`/`Travel`/`Settings` stay if used by the icon map). + +- [ ] **Step 6: Update previews** + +In the two `PreviewHomeScreen*` `Success(...)` constructions, replace the removed booleans with `quickActions = listOf(/* a MultiSelectExpandedLink + two StandaloneQuickLinks */)` so the tiles render in preview. + +- [ ] **Step 7: Compile + lint** + +Run: `./gradlew :feature-home:compileDebugKotlin :app:compileDebugKotlin :feature-home:ktlintCheck` +Expected: BUILD SUCCESSFUL. + +- [ ] **Step 8: Commit** + +```bash +./gradlew ktlintFormat -q +git add -A +git commit -m "feature-home: render quick-action tiles from the member's Help Center actions" +``` + +--- + +## Task 7: Full verification + +- [ ] **Step 1: Build + test the affected modules** + +Run: `./gradlew :member-quick-actions:jvmTest :feature-help-center:compileReleaseKotlin :feature-home:testDebugUnitTest :app:compileDebugKotlin` +Expected: BUILD SUCCESSFUL, all tests green. + +- [ ] **Step 2: ktlint across touched modules** + +Run: `./gradlew :member-quick-actions:ktlintCheck :feature-help-center:ktlintCheck :feature-home:ktlintCheck :app:ktlintCheck` +Expected: BUILD SUCCESSFUL. + +- [ ] **Step 3: Manual smoke (optional)** + +Use the `run` skill / install a debug build; verify Home shows up to three tiles matching the member's Help Center list, "Edit insurance" opens the sub-options sheet, other tiles navigate, and the section disappears when the member has no actions. + +--- + +## Self-Review notes + +- **Spec coverage:** shared module (Task 1-3), Help Center refactor + test move (Task 4), Home presenter + revert gating (Task 5), Home UI + tiles + Edit-insurance sheet + section-hidden-when-empty (Task 6), nav wiring via `toNavKey` (Task 3 + Task 6 Step 1), tests (Task 4 move, Task 5 add). FirstVet included (no exclusion) — icon-mapped and navigable (Task 3 note / Task 6 Step 1). +- **Known unknowns flagged inline:** exact import paths for a few nav keys and whether `FirstVetKey` is shareable (Task 3 Step 2 note); resolve by reading `HelpCenterPresenter.kt`'s current imports. Icon glyph choices are sensible defaults per the spec, adjustable to design. +- **Interface consistency:** `GetMemberQuickActionsUseCase.invoke(): Either>`, `QuickAction` variants, `QuickLinkDestination.toNavKey()`, and `Success.quickActions: List` are used consistently across Tasks 3-6. From b0809c5bff42a33a9818ee1459686e105a7028b6 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 5 Aug 2026 16:37:12 +0200 Subject: [PATCH 18/23] member-quick-actions: scaffold shared module and move member-actions graphql --- .../member-quick-actions/build.gradle.kts | 62 +++++++++++++++++++ ...eryAvailableSelfServiceOnContracts.graphql | 0 .../graphql/QueryMemberActions.graphql | 0 3 files changed, 62 insertions(+) create mode 100644 app/shared/member-quick-actions/build.gradle.kts rename app/{feature/feature-help-center => shared/member-quick-actions}/src/commonMain/graphql/QueryAvailableSelfServiceOnContracts.graphql (100%) rename app/{feature/feature-help-center => shared/member-quick-actions}/src/commonMain/graphql/QueryMemberActions.graphql (100%) diff --git a/app/shared/member-quick-actions/build.gradle.kts b/app/shared/member-quick-actions/build.gradle.kts new file mode 100644 index 0000000000..06f3445466 --- /dev/null +++ b/app/shared/member-quick-actions/build.gradle.kts @@ -0,0 +1,62 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi + +plugins { + id("hedvig.multiplatform.library") + id("hedvig.multiplatform.library.android") + id("hedvig.gradle.plugin") +} + +hedvig { + apollo("octopus") + serialization() +} + +kotlin { + sourceSets { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + applyDefaultHierarchyTemplate { + common { + group("jvmAndAndroid") { + withAndroidLibraryTarget() + withJvm() + } + } + } + commonMain.dependencies { + implementation(libs.apollo.runtime) + implementation(libs.arrow.core) + implementation(libs.arrow.fx) + implementation(libs.coroutines.core) + implementation(libs.kotlinx.serialization.core) + implementation(projects.apolloCore) + implementation(projects.apolloOctopusPublic) + implementation(projects.coreCommonPublic) + implementation(projects.coreResources) + implementation(projects.dataCoinsured) + implementation(projects.featureFlags) + implementation(projects.loggingPublic) + implementation(projects.navigationCommon) + implementation(projects.partnersDeflect) + implementation(projects.uiEmergency) + // -navigation modules for QuickLinkDestination.toNavKey() (Task 3) + implementation(projects.featureChooseTierNavigation) + implementation(projects.featureConnectPaymentTrustlyNavigation) + implementation(projects.featureEditCoinsuredNavigation) + implementation(projects.featureMovingflowNavigation) + implementation(projects.featureTerminateInsuranceNavigation) + implementation(projects.featureTravelCertificateNavigation) + } + jvmTest.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.loggingTest) + } + } +} diff --git a/app/feature/feature-help-center/src/commonMain/graphql/QueryAvailableSelfServiceOnContracts.graphql b/app/shared/member-quick-actions/src/commonMain/graphql/QueryAvailableSelfServiceOnContracts.graphql similarity index 100% rename from app/feature/feature-help-center/src/commonMain/graphql/QueryAvailableSelfServiceOnContracts.graphql rename to app/shared/member-quick-actions/src/commonMain/graphql/QueryAvailableSelfServiceOnContracts.graphql diff --git a/app/feature/feature-help-center/src/commonMain/graphql/QueryMemberActions.graphql b/app/shared/member-quick-actions/src/commonMain/graphql/QueryMemberActions.graphql similarity index 100% rename from app/feature/feature-help-center/src/commonMain/graphql/QueryMemberActions.graphql rename to app/shared/member-quick-actions/src/commonMain/graphql/QueryMemberActions.graphql From a0929cfd1cb75d8fe79668bea8c2409ad87ad966 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 5 Aug 2026 16:44:06 +0200 Subject: [PATCH 19/23] member-quick-actions: move MemberAction + GetMemberActionsUseCase into shared module --- .../memberquickactions/GetMemberActionsUseCaseImpl.kt} | 2 +- .../hedvig/android/memberquickactions}/MemberAction.kt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) rename app/{feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt => shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/GetMemberActionsUseCaseImpl.kt} (98%) rename app/{feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data => shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions}/MemberAction.kt (84%) diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt b/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/GetMemberActionsUseCaseImpl.kt similarity index 98% rename from app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt rename to app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/GetMemberActionsUseCaseImpl.kt index 1e64e37333..0c963f7894 100644 --- a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetMemberActionsUseCase.kt +++ b/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/GetMemberActionsUseCaseImpl.kt @@ -1,4 +1,4 @@ -package com.hedvig.android.feature.help.center.data +package com.hedvig.android.memberquickactions import arrow.core.Either import arrow.core.raise.either diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/MemberAction.kt b/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/MemberAction.kt similarity index 84% rename from app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/MemberAction.kt rename to app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/MemberAction.kt index 93bdf69269..f5f665dd9d 100644 --- a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/MemberAction.kt +++ b/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/MemberAction.kt @@ -1,15 +1,15 @@ -package com.hedvig.android.feature.help.center.data +package com.hedvig.android.memberquickactions 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 { +interface GetMemberActionsUseCase { suspend fun invoke(): Either } -internal data class MemberAction( +data class MemberAction( val isCancelInsuranceEnabled: Boolean, val isConnectPaymentEnabled: Boolean, val isEditCoInsuredEnabled: Boolean, @@ -21,7 +21,7 @@ internal data class MemberAction( val firstVetAction: MemberActionWithDetails.FirstVetAction?, ) -internal sealed interface MemberActionWithDetails { +sealed interface MemberActionWithDetails { data class SickAbroadAction( val deflectData: DeflectData, ) : MemberActionWithDetails From 0511063ddb3272796ace3afc7e8bce17393f5de1 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 5 Aug 2026 16:54:40 +0200 Subject: [PATCH 20/23] member-quick-actions: move QuickAction/QuickLinkDestination + builder + toNavKey into shared module - git mv QuickAction.kt and GetQuickLinksUseCase.kt from feature-help-center into the shared module - Repackage to com.hedvig.android.memberquickactions - Rename interface to GetMemberQuickActionsUseCase and impl to GetMemberQuickActionsUseCaseImpl - Make QuickAction, QuickLinkDestination (all Outer cases), and InnerHelpCenterDestination public - Add QuickLinkNavKey.kt with fun QuickLinkDestination.toNavKey(): HedvigNavKey mapping all Outer cases to nav keys; Inner cases throw error() (caller responsibility) --- .../GetMemberQuickActionsUseCase.kt} | 46 ++++++----- .../memberquickactions}/QuickAction.kt | 3 +- .../memberquickactions/QuickLinkNavKey.kt | 78 +++++++++++++++++++ 3 files changed, 101 insertions(+), 26 deletions(-) rename app/{feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetQuickLinksUseCase.kt => shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/GetMemberQuickActionsUseCase.kt} (90%) rename app/{feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/model => shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions}/QuickAction.kt (81%) create mode 100644 app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/QuickLinkNavKey.kt diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetQuickLinksUseCase.kt b/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/GetMemberQuickActionsUseCase.kt similarity index 90% rename from app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetQuickLinksUseCase.kt rename to app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/GetMemberQuickActionsUseCase.kt index b3cf6635d9..a2607545df 100644 --- a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/data/GetQuickLinksUseCase.kt +++ b/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/GetMemberQuickActionsUseCase.kt @@ -1,4 +1,4 @@ -package com.hedvig.android.feature.help.center.data +package com.hedvig.android.memberquickactions import arrow.core.Either import arrow.core.raise.either @@ -7,14 +7,12 @@ import com.hedvig.android.apollo.ErrorMessage import com.hedvig.android.apollo.safeExecute import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.di.AppScope -import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.ChooseInsuranceForEditCoInsured -import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.ChooseInsuranceForEditCoOwners -import com.hedvig.android.feature.help.center.model.QuickAction -import com.hedvig.android.feature.help.center.model.QuickAction.StandaloneQuickLink import com.hedvig.android.featureflags.FeatureManager import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.logger.LogPriority import com.hedvig.android.logger.logcat +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.ChooseInsuranceForEditCoInsured +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.ChooseInsuranceForEditCoOwners import com.hedvig.android.shared.partners.deflect.DeflectData import com.hedvig.android.ui.emergency.FirstVetSection import dev.zacsweers.metro.ContributesBinding @@ -46,18 +44,18 @@ import hedvig.resources.Res import kotlinx.coroutines.flow.first import octopus.AvailableSelfServiceOnContractsQuery -internal interface GetQuickLinksUseCase { +interface GetMemberQuickActionsUseCase { suspend fun invoke(): Either> } @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) @Inject -internal class GetQuickLinksUseCaseImpl( +internal class GetMemberQuickActionsUseCaseImpl( private val apolloClient: ApolloClient, private val featureManager: FeatureManager, private val getMemberActionsUseCase: GetMemberActionsUseCase, -) : GetQuickLinksUseCase { +) : GetMemberQuickActionsUseCase { override suspend fun invoke(): Either> = either { val memberActionOptions = getMemberActionsUseCase.invoke().bind() @@ -89,7 +87,7 @@ internal class GetQuickLinksUseCaseImpl( } if (memberActionOptions.isTierChangeEnabled) { add( - StandaloneQuickLink( + QuickAction.StandaloneQuickLink( quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkChangeTier, titleRes = Res.string.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_TITLE, hintTextRes = Res.string.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_SUBTITLE, @@ -98,7 +96,7 @@ internal class GetQuickLinksUseCaseImpl( } if (memberActionOptions.isCancelInsuranceEnabled) { add( - StandaloneQuickLink( + QuickAction.StandaloneQuickLink( quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkTermination, titleRes = Res.string.HC_QUICK_ACTIONS_CANCELLATION_TITLE, hintTextRes = Res.string.HC_QUICK_ACTIONS_CANCELLATION_SUBTITLE, @@ -117,7 +115,7 @@ internal class GetQuickLinksUseCaseImpl( } if (memberActionOptions.isMovingEnabled) { add( - StandaloneQuickLink( + QuickAction.StandaloneQuickLink( quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkChangeAddress, titleRes = Res.string.HC_QUICK_ACTIONS_CHANGE_ADDRESS_TITLE, hintTextRes = Res.string.HC_QUICK_ACTIONS_CHANGE_ADDRESS_SUBTITLE, @@ -126,7 +124,7 @@ internal class GetQuickLinksUseCaseImpl( } if (memberActionOptions.isConnectPaymentEnabled) { add( - StandaloneQuickLink( + QuickAction.StandaloneQuickLink( quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkConnectPayment, titleRes = Res.string.HC_QUICK_ACTIONS_PAYMENTS_TITLE, hintTextRes = Res.string.HC_QUICK_ACTIONS_PAYMENTS_SUBTITLE, @@ -135,7 +133,7 @@ internal class GetQuickLinksUseCaseImpl( } if (memberActionOptions.isTravelCertificateEnabled) { add( - StandaloneQuickLink( + QuickAction.StandaloneQuickLink( quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkTravelCertificate, titleRes = Res.string.HC_QUICK_ACTIONS_TRAVEL_CERTIFICATE, hintTextRes = Res.string.HC_QUICK_ACTIONS_TRAVEL_CERTIFICATE_SUBTITLE, @@ -144,7 +142,7 @@ internal class GetQuickLinksUseCaseImpl( } if (memberActionOptions.firstVetAction?.sections?.isNotEmpty() == true) { add( - StandaloneQuickLink( + QuickAction.StandaloneQuickLink( quickLinkDestination = InnerHelpCenterDestination.FirstVet( sections = memberActionOptions.firstVetAction.sections, ), @@ -156,7 +154,7 @@ internal class GetQuickLinksUseCaseImpl( if (memberActionOptions.sickAbroadAction != null) { val deflectData = memberActionOptions.sickAbroadAction.deflectData add( - StandaloneQuickLink( + QuickAction.StandaloneQuickLink( quickLinkDestination = InnerHelpCenterDestination.QuickLinkSickAbroad( deflectData, ), @@ -171,7 +169,7 @@ internal class GetQuickLinksUseCaseImpl( private fun createEditCoInsuredQuickLink( coInsuredContracts: List, -): StandaloneQuickLink? { +): QuickAction.StandaloneQuickLink? { return when { coInsuredContracts.isEmpty() -> { null @@ -180,13 +178,13 @@ private fun createEditCoInsuredQuickLink( coInsuredContracts.size == 1 -> { val contract = coInsuredContracts.first() if (contract.coInsured?.any { it.hasMissingInfo } == true) { - StandaloneQuickLink( + QuickAction.StandaloneQuickLink( quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkCoInsuredAddInfo(contract.id), titleRes = Res.string.HC_QUICK_ACTIONS_CO_INSURED_TITLE, hintTextRes = Res.string.HC_QUICK_ACTIONS_CO_INSURED_SUBTITLE, ) } else { - StandaloneQuickLink( + QuickAction.StandaloneQuickLink( quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkCoInsuredAddOrRemove(contract.id), titleRes = Res.string.HC_QUICK_ACTIONS_CO_INSURED_TITLE, hintTextRes = Res.string.HC_QUICK_ACTIONS_CO_INSURED_SUBTITLE, @@ -195,7 +193,7 @@ private fun createEditCoInsuredQuickLink( } else -> { - StandaloneQuickLink( + QuickAction.StandaloneQuickLink( titleRes = Res.string.HC_QUICK_ACTIONS_EDIT_COINSURED, hintTextRes = Res.string.HC_QUICK_ACTIONS_CO_INSURED_SUBTITLE, quickLinkDestination = ChooseInsuranceForEditCoInsured, @@ -206,7 +204,7 @@ private fun createEditCoInsuredQuickLink( private fun createEditCoOwnersQuickLink( coOwnerContracts: List, -): StandaloneQuickLink? { +): QuickAction.StandaloneQuickLink? { return when { coOwnerContracts.isEmpty() -> { null @@ -215,13 +213,13 @@ private fun createEditCoOwnersQuickLink( coOwnerContracts.size == 1 -> { val contract = coOwnerContracts.first() if (contract.coOwners?.any { it.hasMissingInfo } == true) { - StandaloneQuickLink( + QuickAction.StandaloneQuickLink( quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkCoOwnerAddInfo(contract.id), titleRes = Res.string.CONTRACT_COOWNER, hintTextRes = Res.string.EDIT_COOWNER_SUBTITLE, ) } else { - StandaloneQuickLink( + QuickAction.StandaloneQuickLink( quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkCoOwnerAddOrRemove(contract.id), titleRes = Res.string.CONTRACT_COOWNER, hintTextRes = Res.string.EDIT_COOWNER_SUBTITLE, @@ -230,7 +228,7 @@ private fun createEditCoOwnersQuickLink( } else -> { - StandaloneQuickLink( + QuickAction.StandaloneQuickLink( titleRes = Res.string.EDIT_COOWNER_TITLE, hintTextRes = Res.string.EDIT_COOWNER_SUBTITLE, quickLinkDestination = ChooseInsuranceForEditCoOwners, @@ -265,7 +263,7 @@ sealed interface QuickLinkDestination { } } -internal sealed interface InnerHelpCenterDestination : QuickLinkDestination { +sealed interface InnerHelpCenterDestination : QuickLinkDestination { data class QuickLinkSickAbroad( val deflectData: DeflectData, ) : InnerHelpCenterDestination diff --git a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/model/QuickAction.kt b/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/QuickAction.kt similarity index 81% rename from app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/model/QuickAction.kt rename to app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/QuickAction.kt index 26c1c31436..96ed8b7666 100644 --- a/app/feature/feature-help-center/src/commonMain/kotlin/com/hedvig/android/feature/help/center/model/QuickAction.kt +++ b/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/QuickAction.kt @@ -1,6 +1,5 @@ -package com.hedvig.android.feature.help.center.model +package com.hedvig.android.memberquickactions -import com.hedvig.android.feature.help.center.data.QuickLinkDestination import org.jetbrains.compose.resources.StringResource sealed interface QuickAction { diff --git a/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/QuickLinkNavKey.kt b/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/QuickLinkNavKey.kt new file mode 100644 index 0000000000..95315dc900 --- /dev/null +++ b/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/QuickLinkNavKey.kt @@ -0,0 +1,78 @@ +package com.hedvig.android.memberquickactions + +import com.hedvig.android.data.coinsured.CoInsuredFlowType +import com.hedvig.android.feature.change.tier.navigation.StartTierFlowChooseInsuranceKey +import com.hedvig.android.feature.connect.payment.trustly.ui.TrustlyKey +import com.hedvig.android.feature.editcoinsured.navigation.CoInsuredAddInfoKey +import com.hedvig.android.feature.editcoinsured.navigation.CoInsuredAddOrRemoveKey +import com.hedvig.android.feature.editcoinsured.navigation.EditCoInsuredTriageKey +import com.hedvig.android.feature.movingflow.MovingSource +import com.hedvig.android.feature.movingflow.SelectContractForMovingKey +import com.hedvig.android.feature.terminateinsurance.navigation.TerminateInsuranceKey +import com.hedvig.android.feature.travelcertificate.navigation.TravelCertificateKey +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.ChooseInsuranceForEditCoInsured +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.ChooseInsuranceForEditCoOwners +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkChangeAddress +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkChangeTier +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkCoInsuredAddInfo +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkCoInsuredAddOrRemove +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkCoOwnerAddInfo +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkCoOwnerAddOrRemove +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkConnectPayment +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkTermination +import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkTravelCertificate +import com.hedvig.android.navigation.common.HedvigNavKey + +fun QuickLinkDestination.toNavKey(): HedvigNavKey = when (this) { + is InnerHelpCenterDestination.FirstVet -> { + error("InnerHelpCenterDestination is navigated by the consuming feature, not via toNavKey()") + } + + is InnerHelpCenterDestination.QuickLinkSickAbroad -> { + error("InnerHelpCenterDestination is navigated by the consuming feature, not via toNavKey()") + } + + QuickLinkChangeAddress -> { + SelectContractForMovingKey(MovingSource.OTHER) + } + + is QuickLinkCoInsuredAddInfo -> { + CoInsuredAddInfoKey(contractId, CoInsuredFlowType.CoInsured) + } + + is QuickLinkCoInsuredAddOrRemove -> { + CoInsuredAddOrRemoveKey(contractId, CoInsuredFlowType.CoInsured) + } + + is QuickLinkCoOwnerAddInfo -> { + CoInsuredAddInfoKey(contractId, CoInsuredFlowType.CoOwners) + } + + is QuickLinkCoOwnerAddOrRemove -> { + CoInsuredAddOrRemoveKey(contractId, CoInsuredFlowType.CoOwners) + } + + QuickLinkConnectPayment -> { + TrustlyKey + } + + QuickLinkTermination -> { + TerminateInsuranceKey(null) + } + + QuickLinkTravelCertificate -> { + TravelCertificateKey + } + + QuickLinkChangeTier -> { + StartTierFlowChooseInsuranceKey + } + + ChooseInsuranceForEditCoInsured -> { + EditCoInsuredTriageKey() + } + + ChooseInsuranceForEditCoOwners -> { + EditCoInsuredTriageKey(type = CoInsuredFlowType.CoOwners) + } +} From 6849f504503b4db0780cd5a899ed30e1e537fe92 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 5 Aug 2026 17:08:03 +0200 Subject: [PATCH 21/23] feature-help-center: consume member-quick-actions shared module - Add `implementation(projects.memberQuickActions)` dep to feature-help-center - Update all imports in HelpCenterPresenter, HelpCenterViewModel, HelpCenterHomeDestination, GetInsuranceForEditCoInsuredUseCase to use `com.hedvig.android.memberquickactions.*` instead of old help-center packages - Rename GetQuickLinksUseCase -> GetMemberQuickActionsUseCase references - Replace the big NavigateToQuickAction when-block with a toNavKey() call, intercepting InnerHelpCenterDestination.FirstVet and QuickLinkSickAbroad before delegating to the shared extension - Move GetQuickLinksUseCaseTest -> GetMemberQuickActionsUseCaseTest into member-quick-actions jvmTest, renamed and repackaged - Move GetMemberActionsUseCaseImplTest into member-quick-actions jvmTest (the impl is internal to that module; the test must live there) --- .../feature-help-center/build.gradle.kts | 1 + .../help/center/HelpCenterPresenter.kt | 89 +++---------------- .../help/center/HelpCenterViewModel.kt | 4 +- .../GetInsuranceForEditCoInsuredUseCase.kt | 1 + .../center/home/HelpCenterHomeDestination.kt | 8 +- .../kotlin/GetMemberActionsUseCaseImplTest.kt | 4 +- .../GetMemberQuickActionsUseCaseTest.kt} | 18 ++-- 7 files changed, 30 insertions(+), 95 deletions(-) rename app/{feature/feature-help-center => shared/member-quick-actions}/src/jvmTest/kotlin/GetMemberActionsUseCaseImplTest.kt (95%) rename app/{feature/feature-help-center/src/jvmTest/kotlin/GetQuickLinksUseCaseTest.kt => shared/member-quick-actions/src/jvmTest/kotlin/GetMemberQuickActionsUseCaseTest.kt} (90%) diff --git a/app/feature/feature-help-center/build.gradle.kts b/app/feature/feature-help-center/build.gradle.kts index 29f0b6a783..5ad63a6304 100644 --- a/app/feature/feature-help-center/build.gradle.kts +++ b/app/feature/feature-help-center/build.gradle.kts @@ -61,6 +61,7 @@ kotlin { implementation(projects.navigationCommon) implementation(projects.navigationCompose) implementation(projects.navigationCore) + implementation(projects.memberQuickActions) implementation(projects.partnersDeflect) implementation(projects.uiEmergency) } diff --git a/app/feature/feature-help-center/src/commonMain/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 index 97f4be27f7..50ef0c0ab4 100644 --- a/app/feature/feature-help-center/src/commonMain/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 @@ -11,13 +11,7 @@ import androidx.compose.runtime.setValue import arrow.core.NonEmptyList import arrow.core.merge import arrow.core.toNonEmptyListOrNull -import com.hedvig.android.data.coinsured.CoInsuredFlowType import com.hedvig.android.data.conversations.HasAnyActiveConversationUseCase -import com.hedvig.android.feature.change.tier.navigation.StartTierFlowChooseInsuranceKey -import com.hedvig.android.feature.connect.payment.trustly.ui.TrustlyKey -import com.hedvig.android.feature.editcoinsured.navigation.CoInsuredAddInfoKey -import com.hedvig.android.feature.editcoinsured.navigation.CoInsuredAddOrRemoveKey -import com.hedvig.android.feature.editcoinsured.navigation.EditCoInsuredTriageKey import com.hedvig.android.feature.help.center.HelpCenterEvent.ClearSearchQuery import com.hedvig.android.feature.help.center.HelpCenterEvent.NavigateToQuickAction import com.hedvig.android.feature.help.center.HelpCenterEvent.OnDismissQuickActionDialog @@ -30,29 +24,15 @@ 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.InnerHelpCenterDestination -import com.hedvig.android.feature.help.center.data.QuickLinkDestination -import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.ChooseInsuranceForEditCoInsured -import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.ChooseInsuranceForEditCoOwners -import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.QuickLinkChangeAddress -import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.QuickLinkChangeTier -import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.QuickLinkCoInsuredAddInfo -import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.QuickLinkCoInsuredAddOrRemove -import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.QuickLinkCoOwnerAddInfo -import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.QuickLinkCoOwnerAddOrRemove -import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.QuickLinkConnectPayment -import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.QuickLinkTermination -import com.hedvig.android.feature.help.center.data.QuickLinkDestination.OuterDestination.QuickLinkTravelCertificate -import com.hedvig.android.feature.help.center.model.QuickAction import com.hedvig.android.feature.help.center.navigation.EmergencyKey import com.hedvig.android.feature.help.center.navigation.FirstVetKey -import com.hedvig.android.feature.movingflow.MovingSource -import com.hedvig.android.feature.movingflow.SelectContractForMovingKey -import com.hedvig.android.feature.terminateinsurance.navigation.TerminateInsuranceKey -import com.hedvig.android.feature.travelcertificate.navigation.TravelCertificateKey import com.hedvig.android.featureflags.FeatureManager import com.hedvig.android.featureflags.flags.Feature +import com.hedvig.android.memberquickactions.GetMemberQuickActionsUseCase +import com.hedvig.android.memberquickactions.InnerHelpCenterDestination +import com.hedvig.android.memberquickactions.QuickAction +import com.hedvig.android.memberquickactions.QuickLinkDestination +import com.hedvig.android.memberquickactions.toNavKey import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope import com.hedvig.android.navigation.common.HedvigNavKey @@ -123,7 +103,7 @@ internal data class HelpCenterUiState( } internal class HelpCenterPresenter( - private val getQuickLinksUseCase: GetQuickLinksUseCase, + private val getQuickLinksUseCase: GetMemberQuickActionsUseCase, private val hasAnyActiveConversationUseCase: HasAnyActiveConversationUseCase, private val getHelpCenterFAQUseCase: GetHelpCenterFAQUseCase, private val getPuppyGuideUseCase: GetPuppyGuideUseCase, @@ -176,58 +156,11 @@ internal class HelpCenterPresenter( is NavigateToQuickAction -> { selectedQuickAction = null - val key: HedvigNavKey = when (val destination = event.destination) { - is InnerHelpCenterDestination.FirstVet -> { - FirstVetKey(destination.sections) - } - - is InnerHelpCenterDestination.QuickLinkSickAbroad -> { - EmergencyKey(destination.deflectData) - } - - QuickLinkChangeAddress -> { - SelectContractForMovingKey(MovingSource.OTHER) - } - - is QuickLinkCoInsuredAddInfo -> { - CoInsuredAddInfoKey(destination.contractId, CoInsuredFlowType.CoInsured) - } - - is QuickLinkCoInsuredAddOrRemove -> { - CoInsuredAddOrRemoveKey(destination.contractId, CoInsuredFlowType.CoInsured) - } - - is QuickLinkCoOwnerAddInfo -> { - CoInsuredAddInfoKey(destination.contractId, CoInsuredFlowType.CoOwners) - } - - is QuickLinkCoOwnerAddOrRemove -> { - CoInsuredAddOrRemoveKey(destination.contractId, CoInsuredFlowType.CoOwners) - } - - QuickLinkConnectPayment -> { - TrustlyKey - } - - QuickLinkTermination -> { - TerminateInsuranceKey(null) - } - - QuickLinkTravelCertificate -> { - TravelCertificateKey - } - - QuickLinkChangeTier -> { - StartTierFlowChooseInsuranceKey - } - - ChooseInsuranceForEditCoInsured -> { - EditCoInsuredTriageKey() - } - - ChooseInsuranceForEditCoOwners -> { - EditCoInsuredTriageKey(type = CoInsuredFlowType.CoOwners) - } + val destination = event.destination + val key: HedvigNavKey = when (destination) { + is InnerHelpCenterDestination.FirstVet -> FirstVetKey(destination.sections) + is InnerHelpCenterDestination.QuickLinkSickAbroad -> EmergencyKey(destination.deflectData) + else -> destination.toNavKey() } backstack.add(key) } diff --git a/app/feature/feature-help-center/src/commonMain/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 index bbc67a6f2a..7b62d8f316 100644 --- a/app/feature/feature-help-center/src/commonMain/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 @@ -5,8 +5,8 @@ import com.hedvig.android.core.common.di.HedvigViewModel 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.featureflags.FeatureManager +import com.hedvig.android.memberquickactions.GetMemberQuickActionsUseCase import com.hedvig.android.molecule.public.MoleculeViewModel import com.hedvig.android.navigation.compose.Backstack import dev.zacsweers.metro.Inject @@ -14,7 +14,7 @@ import dev.zacsweers.metro.Inject @Inject @HedvigViewModel(ActivityRetainedScope::class) internal class HelpCenterViewModel( - getQuickLinksUseCase: GetQuickLinksUseCase, + getQuickLinksUseCase: GetMemberQuickActionsUseCase, hasAnyActiveConversationUseCase: HasAnyActiveConversationUseCase, getHelpCenterFAQUseCase: GetHelpCenterFAQUseCase, getPuppyGuideUseCase: GetPuppyGuideUseCase, diff --git a/app/feature/feature-help-center/src/commonMain/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 index 0fa5d2016c..fbca538dc7 100644 --- a/app/feature/feature-help-center/src/commonMain/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 @@ -9,6 +9,7 @@ import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.common.di.AppScope import com.hedvig.android.logger.LogPriority import com.hedvig.android.logger.logcat +import com.hedvig.android.memberquickactions.QuickLinkDestination import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn diff --git a/app/feature/feature-help-center/src/commonMain/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 index 0bd5778c3a..e53a272385 100644 --- a/app/feature/feature-help-center/src/commonMain/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 @@ -95,13 +95,13 @@ import com.hedvig.android.feature.help.center.HelpCenterUiState.PuppyGuidePresen 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.QuickLinkDestination -import com.hedvig.android.feature.help.center.model.QuickAction -import com.hedvig.android.feature.help.center.model.QuickAction.MultiSelectExpandedLink -import com.hedvig.android.feature.help.center.model.QuickAction.StandaloneQuickLink 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.memberquickactions.QuickAction +import com.hedvig.android.memberquickactions.QuickAction.MultiSelectExpandedLink +import com.hedvig.android.memberquickactions.QuickAction.StandaloneQuickLink +import com.hedvig.android.memberquickactions.QuickLinkDestination import com.hedvig.android.placeholder.PlaceholderHighlight import hedvig.resources.HC_CLAIMS_A_01 import hedvig.resources.HC_CLAIMS_Q_01 diff --git a/app/feature/feature-help-center/src/jvmTest/kotlin/GetMemberActionsUseCaseImplTest.kt b/app/shared/member-quick-actions/src/jvmTest/kotlin/GetMemberActionsUseCaseImplTest.kt similarity index 95% rename from app/feature/feature-help-center/src/jvmTest/kotlin/GetMemberActionsUseCaseImplTest.kt rename to app/shared/member-quick-actions/src/jvmTest/kotlin/GetMemberActionsUseCaseImplTest.kt index ae158b2bac..2db7a69b7b 100644 --- a/app/feature/feature-help-center/src/jvmTest/kotlin/GetMemberActionsUseCaseImplTest.kt +++ b/app/shared/member-quick-actions/src/jvmTest/kotlin/GetMemberActionsUseCaseImplTest.kt @@ -8,9 +8,9 @@ import com.hedvig.android.apollo.octopus.test.OctopusFakeResolver import com.hedvig.android.apollo.test.TestApolloClientRule import com.hedvig.android.apollo.test.TestNetworkTransportType import com.hedvig.android.core.common.test.isRight -import com.hedvig.android.feature.help.center.data.GetMemberActionsUseCaseImpl -import com.hedvig.android.feature.help.center.data.MemberAction import com.hedvig.android.logger.TestLogcatLoggingRule +import com.hedvig.android.memberquickactions.GetMemberActionsUseCaseImpl +import com.hedvig.android.memberquickactions.MemberAction import kotlinx.coroutines.test.runTest import octopus.MemberActionsQuery import octopus.type.buildMember diff --git a/app/feature/feature-help-center/src/jvmTest/kotlin/GetQuickLinksUseCaseTest.kt b/app/shared/member-quick-actions/src/jvmTest/kotlin/GetMemberQuickActionsUseCaseTest.kt similarity index 90% rename from app/feature/feature-help-center/src/jvmTest/kotlin/GetQuickLinksUseCaseTest.kt rename to app/shared/member-quick-actions/src/jvmTest/kotlin/GetMemberQuickActionsUseCaseTest.kt index 526307cd33..c526d9a0f3 100644 --- a/app/feature/feature-help-center/src/jvmTest/kotlin/GetQuickLinksUseCaseTest.kt +++ b/app/shared/member-quick-actions/src/jvmTest/kotlin/GetMemberQuickActionsUseCaseTest.kt @@ -10,14 +10,14 @@ import com.hedvig.android.apollo.test.TestApolloClientRule 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.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 -import com.hedvig.android.feature.help.center.model.QuickAction.MultiSelectExpandedLink import com.hedvig.android.featureflags.test.FakeFeatureManager import com.hedvig.android.logger.TestLogcatLoggingRule +import com.hedvig.android.memberquickactions.GetMemberActionsUseCase +import com.hedvig.android.memberquickactions.GetMemberQuickActionsUseCaseImpl +import com.hedvig.android.memberquickactions.MemberAction +import com.hedvig.android.memberquickactions.QuickAction +import com.hedvig.android.memberquickactions.QuickAction.MultiSelectExpandedLink +import com.hedvig.android.memberquickactions.QuickLinkDestination import hedvig.resources.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_SUBTITLE import hedvig.resources.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_TITLE import hedvig.resources.Res @@ -30,7 +30,7 @@ import octopus.type.buildProductVariant import org.junit.Rule import org.junit.Test -class GetQuickLinksUseCaseTest { +class GetMemberQuickActionsUseCaseTest { @get:Rule val testLogcatLogger = TestLogcatLoggingRule() @@ -67,7 +67,7 @@ class GetQuickLinksUseCaseTest { val featureManager = FakeFeatureManager(fixedReturnForAll = true) val getMemberActionsUseCase = FakeGetMemberActionsUseCase() getMemberActionsUseCase.turbine.add(fakeMemberActionWithTier.right()) - val useCase = GetQuickLinksUseCaseImpl( + val useCase = GetMemberQuickActionsUseCaseImpl( 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 = GetQuickLinksUseCaseImpl( + val useCase = GetMemberQuickActionsUseCaseImpl( apolloClient = apolloClientWithGoodResponse, featureManager = featureManager, getMemberActionsUseCase = getMemberActionsUseCase, From 54e7e1bcb994f33495d3d5ec0ae661b1924563d7 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Wed, 5 Aug 2026 17:37:31 +0200 Subject: [PATCH 22/23] feature-home: source quick-action tiles from the shared member use case Home now consumes GetMemberQuickActionsUseCase directly: it fetches the member's quick actions, drops the sick-abroad action (which Home cannot reach) and takes the first three, exposing them as Success.quickActions. The interim memberActions eligibility booleans (isEditInsuranceEnabled / isMovingEnabled / isTravelCertificateEnabled) are removed from the GraphQL query, HomeData, and the ui state. The tiles render one HomeActionTile per action, mapping the action type to an icon; the Edit-insurance MultiSelectExpandedLink opens a Home-owned bottom sheet of its sub-links. Navigation is threaded via a single navigateToQuickLink lambda from :app (routing OuterDestinations through toNavKey()); FirstVet is intercepted inside homeEntries since its key is internal to feature-home. --- app/app/build.gradle.kts | 1 + .../app/navigation/HedvigEntryProvider.kt | 6 +- app/feature/feature-home/build.gradle.kts | 2 + .../src/main/graphql/QueryHome.graphql | 3 - .../home/home/data/GetHomeDataUseCase.kt | 9 - .../home/home/data/GetHomeDataUseCaseDemo.kt | 3 - .../home/home/navigation/HomeEntries.kt | 17 +- .../feature/home/home/ui/HomeDestination.kt | 179 +++++++++++------- .../feature/home/home/ui/HomePresenter.kt | 37 ++-- .../feature/home/home/ui/HomeViewModel.kt | 3 + .../feature/home/home/ui/HomePresenterTest.kt | 156 +++++++++------ 11 files changed, 246 insertions(+), 170 deletions(-) diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 7454577637..87f24cd5dc 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -224,6 +224,7 @@ dependencies { implementation(projects.loggingDeviceModel) implementation(projects.loggingPublic) implementation(projects.permissionCore) + implementation(projects.memberQuickActions) implementation(projects.memberRemindersPublic) implementation(projects.navigationActivity) implementation(projects.navigationCommon) diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt index 41c20a4b81..0823c83eda 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt @@ -15,7 +15,6 @@ import com.hedvig.android.feature.addon.purchase.navigation.AddonPurchaseKey import com.hedvig.android.feature.addon.purchase.navigation.addonPurchaseEntries import com.hedvig.android.feature.change.tier.navigation.ChooseTierKey import com.hedvig.android.feature.change.tier.navigation.InsuranceCustomizationParameters -import com.hedvig.android.feature.change.tier.navigation.StartTierFlowChooseInsuranceKey import com.hedvig.android.feature.change.tier.navigation.StartTierFlowKey import com.hedvig.android.feature.change.tier.navigation.changeTierEntries import com.hedvig.android.feature.chat.navigation.ChatKey @@ -60,6 +59,7 @@ import com.hedvig.android.feature.travelcertificate.navigation.TravelCertificate import com.hedvig.android.feature.travelcertificate.navigation.travelCertificateEntries import com.hedvig.android.language.LanguageService import com.hedvig.android.logger.logcat +import com.hedvig.android.memberquickactions.toNavKey import com.hedvig.android.navigation.activity.ExternalNavigator import com.hedvig.android.navigation.common.HedvigNavKey import com.hedvig.android.navigation.common.TopLevelTab @@ -250,8 +250,7 @@ private fun EntryProviderScope.addHomeEntries( backstack.add(CoInsuredAddInfoKey(contractId, type)) }, navigateToHelpCenter = { backstack.add(HelpCenterKey) }, - navigateToMovingFlow = { navigateToMovingFlow(MovingSource.OTHER) }, - navigateToEditInsurance = { backstack.add(StartTierFlowChooseInsuranceKey) }, + navigateToQuickLink = { destination -> backstack.add(destination.toNavKey()) }, navigateToClaimChat = { resumeClaim -> backstack.add( ClaimChatKey( @@ -266,7 +265,6 @@ private fun EntryProviderScope.addHomeEntries( openUrl = openUrl, openCrossSellUrl = openCrossSellUrl, imageLoader = imageLoader, - navigateToTravelCertificate = navigateToTravelCertificate, navigateToAddonPurchaseFlow = navigateToAddonPurchaseFlow, ) } diff --git a/app/feature/feature-home/build.gradle.kts b/app/feature/feature-home/build.gradle.kts index c86627faa7..83bb01edf9 100644 --- a/app/feature/feature-home/build.gradle.kts +++ b/app/feature/feature-home/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { implementation(projects.designSystemHedvig) implementation(projects.featureFlags) implementation(projects.languageCore) + implementation(projects.memberQuickActions) implementation(projects.memberRemindersPublic) implementation(projects.memberRemindersUi) implementation(projects.moleculePublic) @@ -72,5 +73,6 @@ dependencies { testImplementation(projects.loggingTest) testImplementation(projects.memberRemindersTest) testImplementation(projects.moleculeTest) + testImplementation(projects.partnersDeflect) testImplementation(projects.testClock) } diff --git a/app/feature/feature-home/src/main/graphql/QueryHome.graphql b/app/feature/feature-home/src/main/graphql/QueryHome.graphql index 1ab4696adc..0d29fa7dd7 100644 --- a/app/feature/feature-home/src/main/graphql/QueryHome.graphql +++ b/app/feature/feature-home/src/main/graphql/QueryHome.graphql @@ -81,9 +81,6 @@ query Home($claimsHistoryFlag: Boolean!, $resumeClaimEnabled: Boolean!) { } } memberActions { - isChangeTierEnabled - isMovingEnabled - isTravelCertificateEnabled firstVetAction { sections { title diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt index c62821a01e..e884924629 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt @@ -200,10 +200,6 @@ internal class GetHomeDataUseCaseImpl( memberReminders = memberReminders, hasUnseenChatMessages = hasUnseenChatMessages, showHelpCenter = true, - isEditInsuranceEnabled = homeQueryData.currentMember.memberActions?.isChangeTierEnabled ?: false, - isMovingEnabled = homeQueryData.currentMember.memberActions?.isMovingEnabled ?: false, - isTravelCertificateEnabled = homeQueryData.currentMember.memberActions?.isTravelCertificateEnabled - ?: false, firstVetSections = firstVetActions, crossSells = crossSells, addonBannerInfos = travelBannerInfo.orEmpty(), @@ -322,11 +318,6 @@ data class HomeData( val showChatIcon: Boolean, val hasUnseenChatMessages: Boolean, val showHelpCenter: Boolean, - // Per-member eligibility for each quick-action tile; a tile is hidden when its flag is false, and the - // whole quick-actions section is hidden when none are enabled. - val isEditInsuranceEnabled: Boolean, - val isMovingEnabled: Boolean, - val isTravelCertificateEnabled: Boolean, val firstVetSections: List, val crossSells: CrossSellSheetData, val addonBannerInfos: List, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt index 96688273d3..c445223692 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt @@ -26,9 +26,6 @@ internal class GetHomeDataUseCaseDemo : GetHomeDataUseCase { ), hasUnseenChatMessages = false, showHelpCenter = true, - isEditInsuranceEnabled = true, - isMovingEnabled = true, - isTravelCertificateEnabled = true, firstVetSections = listOf(), crossSells = CrossSellSheetData( recommendedCrossSell = diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt index 452946d427..ce6e0c09a0 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt @@ -8,6 +8,8 @@ import com.hedvig.android.data.coinsured.CoInsuredFlowType import com.hedvig.android.feature.home.home.ui.FirstVetDestination import com.hedvig.android.feature.home.home.ui.HomeDestination import com.hedvig.android.feature.home.home.ui.HomeViewModel +import com.hedvig.android.memberquickactions.InnerHelpCenterDestination +import com.hedvig.android.memberquickactions.QuickLinkDestination import com.hedvig.android.navigation.common.HedvigNavKey import com.hedvig.android.navigation.compose.Backstack import com.hedvig.android.navigation.compose.NavSuiteSceneDecoratorStrategy @@ -25,15 +27,13 @@ fun EntryProviderScope.homeEntries( navigateToContactInfo: () -> Unit, navigateToMissingInfo: (String, CoInsuredFlowType) -> Unit, navigateToHelpCenter: () -> Unit, - navigateToMovingFlow: () -> Unit, - navigateToEditInsurance: () -> Unit, + navigateToQuickLink: (QuickLinkDestination) -> Unit, navigateToClaimChat: (resumeClaim: Boolean) -> Unit, navigateToChipIdScreen: () -> Unit, openAppSettings: () -> Unit, openUrl: (String) -> Unit, openCrossSellUrl: (String) -> Unit, imageLoader: ImageLoader, - navigateToTravelCertificate: () -> Unit, navigateToAddonPurchaseFlow: (List) -> Unit, ) { entry(metadata = NavSuiteSceneDecoratorStrategy.showNavBar()) { @@ -50,8 +50,14 @@ fun EntryProviderScope.homeEntries( navigateToConnectPayout = dropUnlessResumed { navigateToConnectPayout() }, navigateToMissingInfo = dropUnlessResumed { contractId, type -> navigateToMissingInfo(contractId, type) }, navigateToHelpCenter = dropUnlessResumed { navigateToHelpCenter() }, - navigateToMovingFlow = dropUnlessResumed { navigateToMovingFlow() }, - navigateToEditInsurance = dropUnlessResumed { navigateToEditInsurance() }, + navigateToQuickLink = dropUnlessResumed { destination -> + // FirstVet lives inside feature-home; every other destination is routed by the caller. + if (destination is InnerHelpCenterDestination.FirstVet) { + backstack.add(FirstVetKey(destination.sections)) + } else { + navigateToQuickLink(destination) + } + }, openUrl = openUrl, openCrossSellUrl = openCrossSellUrl, openAppSettings = openAppSettings, @@ -63,7 +69,6 @@ fun EntryProviderScope.homeEntries( }, imageLoader = imageLoader, navigateToChipId = navigateToChipIdScreen, - navigateToTravelCertificate = dropUnlessResumed { navigateToTravelCertificate() }, navigateToAddonPurchaseFlow = dropUnlessResumed { ids -> navigateToAddonPurchaseFlow(ids) }, ) } diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 209ed42612..50a27a9cf7 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed @@ -110,6 +111,7 @@ import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonStyle.Second import com.hedvig.android.design.system.hedvig.DraftClaimDialog import com.hedvig.android.design.system.hedvig.ErrorDialog import com.hedvig.android.design.system.hedvig.HedvigAlertDialog +import com.hedvig.android.design.system.hedvig.HedvigBottomSheet import com.hedvig.android.design.system.hedvig.HedvigButton import com.hedvig.android.design.system.hedvig.HedvigCard import com.hedvig.android.design.system.hedvig.HedvigErrorSection @@ -129,7 +131,9 @@ import com.hedvig.android.design.system.hedvig.TooltipDefaults.TooltipStyle.Inbo import com.hedvig.android.design.system.hedvig.TopAppBarLayoutForActions import com.hedvig.android.design.system.hedvig.api.HedvigBottomSheetState import com.hedvig.android.design.system.hedvig.hedvigDropShadow +import com.hedvig.android.design.system.hedvig.icon.Card import com.hedvig.android.design.system.hedvig.icon.HedvigIcons +import com.hedvig.android.design.system.hedvig.icon.HelipadOutline import com.hedvig.android.design.system.hedvig.icon.Reload import com.hedvig.android.design.system.hedvig.icon.Settings import com.hedvig.android.design.system.hedvig.icon.Travel @@ -146,6 +150,9 @@ import com.hedvig.android.feature.home.home.ui.HomeTopBarAction.ChatAction import com.hedvig.android.feature.home.home.ui.HomeTopBarAction.CrossSellsAction import com.hedvig.android.feature.home.home.ui.HomeTopBarAction.FirstVetAction import com.hedvig.android.feature.home.home.ui.HomeUiState.Success +import com.hedvig.android.memberquickactions.InnerHelpCenterDestination +import com.hedvig.android.memberquickactions.QuickAction +import com.hedvig.android.memberquickactions.QuickLinkDestination import com.hedvig.android.memberreminders.MemberReminder import com.hedvig.android.memberreminders.MemberReminder.PaymentReminder.ConnectPayment import com.hedvig.android.memberreminders.MemberReminders @@ -172,15 +179,20 @@ import com.hedvig.android.ui.claimstatus.model.ClaimStatusCardUiState import com.hedvig.android.ui.emergency.FirstVetSection import hedvig.resources.CHAT_NEW_MESSAGE import hedvig.resources.DASHBOARD_OPEN_CHAT +import hedvig.resources.HC_QUICK_ACTIONS_CHANGE_ADDRESS_SUBTITLE +import hedvig.resources.HC_QUICK_ACTIONS_CHANGE_ADDRESS_TITLE +import hedvig.resources.HC_QUICK_ACTIONS_EDIT_INSURANCE_SUBTITLE +import hedvig.resources.HC_QUICK_ACTIONS_EDIT_INSURANCE_TITLE +import hedvig.resources.HC_QUICK_ACTIONS_PAYMENTS_SUBTITLE +import hedvig.resources.HC_QUICK_ACTIONS_PAYMENTS_TITLE import hedvig.resources.HC_QUICK_ACTIONS_TITLE -import hedvig.resources.HC_QUICK_ACTIONS_TRAVEL_CERTIFICATE +import hedvig.resources.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_SUBTITLE +import hedvig.resources.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_TITLE import hedvig.resources.HOME_ADDONS_READ_MORE_BUTTON import hedvig.resources.HOME_DISCOVER_SECTION_TITLE import hedvig.resources.HOME_DISCOVER_SEE_PRICE_BUTTON import hedvig.resources.HOME_GREETING_SUBTITLE import hedvig.resources.HOME_GREETING_TITLE -import hedvig.resources.HOME_QUICK_ACTIONS_CHANGE_ADDRESS -import hedvig.resources.HOME_QUICK_ACTIONS_EDIT_INSURANCE import hedvig.resources.HOME_QUOTES_SECTION_TITLE import hedvig.resources.HOME_TODO_SECTION_TITLE import hedvig.resources.INSURANCE_ADDONS_SUBHEADING @@ -220,8 +232,7 @@ internal fun HomeDestination( navigateToConnectPayment: () -> Unit, navigateToConnectPayout: () -> Unit, navigateToHelpCenter: () -> Unit, - navigateToMovingFlow: () -> Unit, - navigateToEditInsurance: () -> Unit, + navigateToQuickLink: (QuickLinkDestination) -> Unit, openUrl: (String) -> Unit, openCrossSellUrl: (String) -> Unit, openAppSettings: () -> Unit, @@ -230,7 +241,6 @@ internal fun HomeDestination( navigateToContactInfo: () -> Unit, navigateToChipId: () -> Unit, imageLoader: ImageLoader, - navigateToTravelCertificate: () -> Unit, navigateToAddonPurchaseFlow: (List) -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -246,8 +256,7 @@ internal fun HomeDestination( navigateToConnectPayment = navigateToConnectPayment, navigateToConnectPayout = navigateToConnectPayout, navigateToHelpCenter = navigateToHelpCenter, - navigateToMovingFlow = navigateToMovingFlow, - navigateToEditInsurance = navigateToEditInsurance, + navigateToQuickLink = navigateToQuickLink, openUrl = openUrl, openCrossSellUrl = openCrossSellUrl, openAppSettings = openAppSettings, @@ -262,7 +271,6 @@ internal fun HomeDestination( viewModel.emit(HomeEvent.CrossSellToolTipShown(epochDay)) }, imageLoader = imageLoader, - navigateToTravelCertificate = navigateToTravelCertificate, navigateToAddonPurchaseFlow = navigateToAddonPurchaseFlow, ) } @@ -279,8 +287,7 @@ private fun HomeScreen( navigateToConnectPayment: () -> Unit, navigateToConnectPayout: () -> Unit, navigateToHelpCenter: () -> Unit, - navigateToMovingFlow: () -> Unit, - navigateToEditInsurance: () -> Unit, + navigateToQuickLink: (QuickLinkDestination) -> Unit, openUrl: (String) -> Unit, openCrossSellUrl: (String) -> Unit, markMessageAsSeen: (String) -> Unit, @@ -293,7 +300,6 @@ private fun HomeScreen( markCrossSellsNotificationAsSeen: () -> Unit, setEpochDayWhenLastToolTipShown: (Long) -> Unit, imageLoader: ImageLoader, - navigateToTravelCertificate: () -> Unit, navigateToAddonPurchaseFlow: (List) -> Unit, ) { val systemBarInsetTopDp = with(LocalDensity.current) { @@ -312,6 +318,12 @@ private fun HomeScreen( imageLoader = imageLoader, ) + val editInsuranceSheetState = rememberHedvigBottomSheetState() + EditInsuranceQuickActionSheet( + state = editInsuranceSheetState, + onQuickLink = navigateToQuickLink, + ) + val startClaimBottomSheetState = rememberHedvigBottomSheetState() StartClaimBottomSheet( state = startClaimBottomSheetState, @@ -397,8 +409,8 @@ private fun HomeScreen( navigateToConnectPayment = navigateToConnectPayment, navigateToConnectPayout = navigateToConnectPayout, navigateToHelpCenter = navigateToHelpCenter, - navigateToMovingFlow = navigateToMovingFlow, - navigateToEditInsurance = navigateToEditInsurance, + navigateToQuickLink = navigateToQuickLink, + onEditInsurance = { editInsuranceSheetState.show(it) }, onNavigateToInbox = onNavigateToInbox, openClaimFlowSheet = { if (draftClaim != null) { @@ -426,7 +438,6 @@ private fun HomeScreen( navigateToChipIdScreen = navigateToChipIdScreen, openCrossSellUrl = openCrossSellUrl, imageLoader = imageLoader, - navigateToTravelCertificate = navigateToTravelCertificate, navigateToAddonPurchaseFlow = navigateToAddonPurchaseFlow, ) } @@ -568,8 +579,8 @@ private fun HomeScreenSuccess( navigateToConnectPayment: () -> Unit, navigateToConnectPayout: () -> Unit, navigateToHelpCenter: () -> Unit, - navigateToMovingFlow: () -> Unit, - navigateToEditInsurance: () -> Unit, + navigateToQuickLink: (QuickLinkDestination) -> Unit, + onEditInsurance: (QuickAction.MultiSelectExpandedLink) -> Unit, onNavigateToInbox: () -> Unit, openClaimFlowSheet: () -> Unit, onContinueDraftClaim: () -> Unit, @@ -583,7 +594,6 @@ private fun HomeScreenSuccess( navigateToChipIdScreen: () -> Unit, openCrossSellUrl: (String) -> Unit, imageLoader: ImageLoader, - navigateToTravelCertificate: () -> Unit, navigateToAddonPurchaseFlow: (List) -> Unit, modifier: Modifier = Modifier, ) { @@ -663,7 +673,7 @@ private fun HomeScreenSuccess( } HomeSection.QuickActionTiles -> { - uiState.isEditInsuranceEnabled || uiState.isMovingEnabled || uiState.isTravelCertificateEnabled + uiState.quickActions.isNotEmpty() } } } @@ -901,12 +911,9 @@ private fun HomeScreenSuccess( ) HomeSection.QuickActionTiles -> QuickActionTilesSection( - isEditInsuranceEnabled = uiState.isEditInsuranceEnabled, - isMovingEnabled = uiState.isMovingEnabled, - isTravelCertificateEnabled = uiState.isTravelCertificateEnabled, - onEditInsurance = navigateToEditInsurance, - onChangeAddress = navigateToMovingFlow, - onTravelCertificate = navigateToTravelCertificate, + quickActions = uiState.quickActions, + onQuickLink = navigateToQuickLink, + onEditInsurance = onEditInsurance, horizontalInsets = horizontalInsets, ) } @@ -1164,12 +1171,9 @@ private fun OffersSection( @Composable private fun QuickActionTilesSection( - isEditInsuranceEnabled: Boolean, - isMovingEnabled: Boolean, - isTravelCertificateEnabled: Boolean, - onEditInsurance: () -> Unit, - onChangeAddress: () -> Unit, - onTravelCertificate: () -> Unit, + quickActions: List, + onQuickLink: (QuickLinkDestination) -> Unit, + onEditInsurance: (QuickAction.MultiSelectExpandedLink) -> Unit, horizontalInsets: PaddingValues, ) { Column( @@ -1190,37 +1194,64 @@ private fun QuickActionTilesSection( .fillMaxWidth() .height(IntrinsicSize.Max), ) { - if (isEditInsuranceEnabled) { + quickActions.forEach { action -> HomeActionTile( - icon = HedvigIcons.Settings, - text = stringResource(Res.string.HOME_QUICK_ACTIONS_EDIT_INSURANCE), - onClick = onEditInsurance, - modifier = Modifier - .weight(1f) - .fillMaxHeight(), - ) - } - if (isMovingEnabled) { - HomeActionTile( - icon = HedvigIcons.Reload, - text = stringResource(Res.string.HOME_QUICK_ACTIONS_CHANGE_ADDRESS), - onClick = onChangeAddress, + icon = action.homeIcon(), + text = stringResource(action.titleRes), + onClick = { + when (action) { + is QuickAction.StandaloneQuickLink -> onQuickLink(action.quickLinkDestination) + is QuickAction.MultiSelectExpandedLink -> onEditInsurance(action) + } + }, modifier = Modifier .weight(1f) .fillMaxHeight(), ) } - if (isTravelCertificateEnabled) { + } + } +} + +private fun QuickAction.homeIcon(): ImageVector = when (this) { + is QuickAction.MultiSelectExpandedLink -> HedvigIcons.Settings + + is QuickAction.StandaloneQuickLink -> when (quickLinkDestination) { + QuickLinkDestination.OuterDestination.QuickLinkChangeAddress -> HedvigIcons.Reload + QuickLinkDestination.OuterDestination.QuickLinkConnectPayment -> HedvigIcons.Card + QuickLinkDestination.OuterDestination.QuickLinkTravelCertificate -> HedvigIcons.Travel + is InnerHelpCenterDestination.FirstVet -> HedvigIcons.HelipadOutline + else -> HedvigIcons.Settings + } +} + +@Composable +private fun EditInsuranceQuickActionSheet( + state: HedvigBottomSheetState, + onQuickLink: (QuickLinkDestination) -> Unit, +) { + HedvigBottomSheet(state) { editInsurance -> + HedvigText( + text = stringResource(editInsurance.titleRes), + style = HedvigTheme.typography.headlineSmall, + modifier = Modifier.semantics { heading() }, + ) + Spacer(Modifier.height(16.dp)) + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + editInsurance.links.forEach { link -> HomeActionTile( - icon = HedvigIcons.Travel, - text = stringResource(Res.string.HC_QUICK_ACTIONS_TRAVEL_CERTIFICATE), - onClick = onTravelCertificate, - modifier = Modifier - .weight(1f) - .fillMaxHeight(), + icon = link.homeIcon(), + text = stringResource(link.titleRes), + onClick = { + state.dismiss() + onQuickLink(link.quickLinkDestination) + }, + modifier = Modifier.fillMaxWidth(), ) } } + Spacer(Modifier.height(8.dp)) + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) } } @@ -1443,9 +1474,7 @@ private fun PreviewHomeScreen( connectPayment = ConnectPayment(), ), isHelpCenterEnabled = true, - isEditInsuranceEnabled = true, - isMovingEnabled = true, - isTravelCertificateEnabled = true, + quickActions = previewQuickActions, hasUnseenChatMessages = hasUnseenChatMessages, crossSellsPartition = CrossSellsPartition( offersCrossSell = RecommendedCrossSell( @@ -1530,8 +1559,7 @@ private fun PreviewHomeScreen( navigateToConnectPayment = {}, navigateToConnectPayout = {}, navigateToHelpCenter = {}, - navigateToMovingFlow = {}, - navigateToEditInsurance = {}, + navigateToQuickLink = {}, openUrl = {}, openCrossSellUrl = {}, openAppSettings = {}, @@ -1544,7 +1572,6 @@ private fun PreviewHomeScreen( navigateToChipIdScreen = {}, setEpochDayWhenLastToolTipShown = {}, imageLoader = rememberPreviewImageLoader(), - navigateToTravelCertificate = {}, navigateToAddonPurchaseFlow = {}, ) } @@ -1567,8 +1594,7 @@ private fun PreviewHomeScreenWithError() { navigateToConnectPayment = {}, navigateToConnectPayout = {}, navigateToHelpCenter = {}, - navigateToMovingFlow = {}, - navigateToEditInsurance = {}, + navigateToQuickLink = {}, openUrl = {}, openCrossSellUrl = {}, openAppSettings = {}, @@ -1581,7 +1607,6 @@ private fun PreviewHomeScreenWithError() { navigateToChipIdScreen = {}, setEpochDayWhenLastToolTipShown = {}, imageLoader = rememberPreviewImageLoader(), - navigateToTravelCertificate = {}, navigateToAddonPurchaseFlow = {}, ) } @@ -1609,9 +1634,7 @@ private fun PreviewHomeScreenAllHomeTextTypes( updateContactInfo = null, ), isHelpCenterEnabled = false, - isEditInsuranceEnabled = true, - isMovingEnabled = true, - isTravelCertificateEnabled = true, + quickActions = previewQuickActions, hasUnseenChatMessages = false, crossSellsAction = null, firstVetAction = null, @@ -1629,8 +1652,7 @@ private fun PreviewHomeScreenAllHomeTextTypes( navigateToConnectPayment = {}, navigateToConnectPayout = {}, navigateToHelpCenter = {}, - navigateToMovingFlow = {}, - navigateToEditInsurance = {}, + navigateToQuickLink = {}, openUrl = {}, openCrossSellUrl = {}, openAppSettings = {}, @@ -1643,13 +1665,36 @@ private fun PreviewHomeScreenAllHomeTextTypes( navigateToChipIdScreen = {}, setEpochDayWhenLastToolTipShown = {}, imageLoader = rememberPreviewImageLoader(), - navigateToTravelCertificate = {}, navigateToAddonPurchaseFlow = {}, ) } } } +private val previewQuickActions: List = listOf( + QuickAction.MultiSelectExpandedLink( + titleRes = Res.string.HC_QUICK_ACTIONS_EDIT_INSURANCE_TITLE, + hintTextRes = Res.string.HC_QUICK_ACTIONS_EDIT_INSURANCE_SUBTITLE, + links = listOf( + QuickAction.StandaloneQuickLink( + titleRes = Res.string.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_TITLE, + hintTextRes = Res.string.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_SUBTITLE, + quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkChangeTier, + ), + ), + ), + QuickAction.StandaloneQuickLink( + titleRes = Res.string.HC_QUICK_ACTIONS_CHANGE_ADDRESS_TITLE, + hintTextRes = Res.string.HC_QUICK_ACTIONS_CHANGE_ADDRESS_SUBTITLE, + quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkChangeAddress, + ), + QuickAction.StandaloneQuickLink( + titleRes = Res.string.HC_QUICK_ACTIONS_PAYMENTS_TITLE, + hintTextRes = Res.string.HC_QUICK_ACTIONS_PAYMENTS_SUBTITLE, + quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkConnectPayment, + ), +) + private class HomeTextPreviewParameterProvider : CollectionPreviewParameterProvider( listOf( Active, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt index 6653affff2..d6c6a746e6 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.Snapshot import arrow.core.Either +import arrow.core.getOrElse import com.hedvig.android.apollo.ApolloOperationError import com.hedvig.android.core.common.ApplicationScope import com.hedvig.android.crosssells.CrossSellSheetData @@ -22,6 +23,9 @@ import com.hedvig.android.feature.home.home.data.HomeData import com.hedvig.android.feature.home.home.data.SeenImportantMessagesStorage import com.hedvig.android.logger.LogPriority import com.hedvig.android.logger.logcat +import com.hedvig.android.memberquickactions.GetMemberQuickActionsUseCase +import com.hedvig.android.memberquickactions.InnerHelpCenterDestination +import com.hedvig.android.memberquickactions.QuickAction import com.hedvig.android.memberreminders.MemberReminders import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope @@ -42,6 +46,7 @@ internal class HomePresenter( private val applicationScope: ApplicationScope, private val isProduction: Boolean, private val deleteClaimIntentDraftUseCase: DeleteClaimIntentDraftUseCase, + private val getMemberQuickActionsUseCase: GetMemberQuickActionsUseCase, ) : MoleculePresenter { @Composable override fun MoleculePresenterScope.present(lastState: HomeUiState): HomeUiState { @@ -121,10 +126,14 @@ internal class HomePresenter( } }, ) { homeData: HomeData -> + val quickActions = getMemberQuickActionsUseCase.invoke() + .getOrElse { emptyList() } + .filterNot { it.isSickAbroad() } + .take(3) Snapshot.withMutableSnapshot { hasError = false isReloading = false - successData = SuccessData.fromHomeData(homeData, crossSellNotification) + successData = SuccessData.fromHomeData(homeData, crossSellNotification, quickActions) } } } @@ -147,9 +156,7 @@ internal class HomePresenter( !alreadySeenImportantMessages.contains(it.id) }, isHelpCenterEnabled = successData.showHelpCenter, - isEditInsuranceEnabled = successData.isEditInsuranceEnabled, - isMovingEnabled = successData.isMovingEnabled, - isTravelCertificateEnabled = successData.isTravelCertificateEnabled, + quickActions = successData.quickActions, hasUnseenChatMessages = successData.hasUnseenChatMessages, chatAction = successData.chatAction, firstVetAction = successData.firstVetAction, @@ -165,6 +172,11 @@ internal class HomePresenter( } } +// Home cannot navigate to the sick-abroad emergency screen (it lives in feature-help-center), so that +// quick action is dropped from the Home tiles. +private fun QuickAction.isSickAbroad(): Boolean = this is QuickAction.StandaloneQuickLink && + quickLinkDestination is InnerHelpCenterDestination.QuickLinkSickAbroad + internal sealed interface HomeEvent { data object RefreshData : HomeEvent @@ -196,9 +208,7 @@ internal sealed interface HomeUiState { val addonBannerInfos: List, val isProduction: Boolean, val isHelpCenterEnabled: Boolean, - val isEditInsuranceEnabled: Boolean, - val isMovingEnabled: Boolean, - val isTravelCertificateEnabled: Boolean, + val quickActions: List, override val hasUnseenChatMessages: Boolean, val crossSellsPartition: CrossSellsPartition = CrossSellsPartition(), val firstName: String = "", @@ -216,9 +226,7 @@ private data class SuccessData( val veryImportantMessages: List, val memberReminders: MemberReminders, val showHelpCenter: Boolean, - val isEditInsuranceEnabled: Boolean, - val isMovingEnabled: Boolean, - val isTravelCertificateEnabled: Boolean, + val quickActions: List, val chatAction: HomeTopBarAction.ChatAction?, val firstVetAction: HomeTopBarAction.FirstVetAction?, val crossSellsAction: HomeTopBarAction.CrossSellsAction?, @@ -237,9 +245,7 @@ private data class SuccessData( veryImportantMessages = lastState.veryImportantMessages, memberReminders = lastState.memberReminders, showHelpCenter = lastState.isHelpCenterEnabled, - isEditInsuranceEnabled = lastState.isEditInsuranceEnabled, - isMovingEnabled = lastState.isMovingEnabled, - isTravelCertificateEnabled = lastState.isTravelCertificateEnabled, + quickActions = lastState.quickActions, crossSellsAction = lastState.crossSellsAction, firstVetAction = lastState.firstVetAction, hasUnseenChatMessages = lastState.hasUnseenChatMessages, @@ -254,6 +260,7 @@ private data class SuccessData( fun fromHomeData( homeData: HomeData, crossSellRecommendationNotification: CrossSellRecommendationNotification, + quickActions: List, ): SuccessData { val crossSellsAction = if (homeData.crossSells.recommendedCrossSell != null || homeData.crossSells.recommendedAddon != null || @@ -291,9 +298,7 @@ private data class SuccessData( enableNotifications = null, ), showHelpCenter = homeData.showHelpCenter, - isEditInsuranceEnabled = homeData.isEditInsuranceEnabled, - isMovingEnabled = homeData.isMovingEnabled, - isTravelCertificateEnabled = homeData.isTravelCertificateEnabled, + quickActions = quickActions, firstVetAction = firstVetAction, crossSellsAction = crossSellsAction, hasUnseenChatMessages = homeData.hasUnseenChatMessages, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeViewModel.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeViewModel.kt index 92584d1d60..8136018a6c 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeViewModel.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeViewModel.kt @@ -7,6 +7,7 @@ import com.hedvig.android.core.common.di.HedvigViewModel import com.hedvig.android.data.claimintent.DeleteClaimIntentDraftUseCase import com.hedvig.android.feature.home.home.data.GetHomeDataUseCase import com.hedvig.android.feature.home.home.data.SeenImportantMessagesStorage +import com.hedvig.android.memberquickactions.GetMemberQuickActionsUseCase import com.hedvig.android.molecule.public.MoleculeViewModel import com.hedvig.android.notification.badge.data.crosssell.home.CrossSellHomeNotificationService import dev.zacsweers.metro.Inject @@ -20,6 +21,7 @@ internal class HomeViewModel( applicationScope: ApplicationScope, hedvigBuildConstants: HedvigBuildConstants, deleteClaimIntentDraftUseCase: DeleteClaimIntentDraftUseCase, + getMemberQuickActionsUseCase: GetMemberQuickActionsUseCase, ) : MoleculeViewModel( HomeUiState.Loading, HomePresenter( @@ -29,5 +31,6 @@ internal class HomeViewModel( applicationScope, hedvigBuildConstants.isProduction, deleteClaimIntentDraftUseCase, + getMemberQuickActionsUseCase, ), ) diff --git a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt index 064279defb..65b91b7e58 100644 --- a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt +++ b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt @@ -26,12 +26,28 @@ import com.hedvig.android.feature.home.home.data.GetHomeDataUseCase import com.hedvig.android.feature.home.home.data.HomeData import com.hedvig.android.feature.home.home.data.SeenImportantMessagesStorageImpl import com.hedvig.android.logger.TestLogcatLoggingRule +import com.hedvig.android.memberquickactions.GetMemberQuickActionsUseCase +import com.hedvig.android.memberquickactions.InnerHelpCenterDestination +import com.hedvig.android.memberquickactions.QuickAction +import com.hedvig.android.memberquickactions.QuickLinkDestination import com.hedvig.android.memberreminders.MemberReminder import com.hedvig.android.memberreminders.MemberReminders import com.hedvig.android.molecule.test.test import com.hedvig.android.notification.badge.data.crosssell.home.CrossSellHomeNotificationService +import com.hedvig.android.shared.partners.deflect.DeflectData import com.hedvig.android.ui.claimstatus.model.ClaimStatusCardUiState import com.hedvig.android.ui.emergency.FirstVetSection +import hedvig.resources.HC_QUICK_ACTIONS_CHANGE_ADDRESS_SUBTITLE +import hedvig.resources.HC_QUICK_ACTIONS_CHANGE_ADDRESS_TITLE +import hedvig.resources.HC_QUICK_ACTIONS_EDIT_INSURANCE_SUBTITLE +import hedvig.resources.HC_QUICK_ACTIONS_EDIT_INSURANCE_TITLE +import hedvig.resources.HC_QUICK_ACTIONS_PAYMENTS_SUBTITLE +import hedvig.resources.HC_QUICK_ACTIONS_PAYMENTS_TITLE +import hedvig.resources.HC_QUICK_ACTIONS_SICK_ABROAD_SUBTITLE +import hedvig.resources.HC_QUICK_ACTIONS_SICK_ABROAD_TITLE +import hedvig.resources.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_SUBTITLE +import hedvig.resources.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_TITLE +import hedvig.resources.Res import kotlin.time.Instant import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf @@ -83,6 +99,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) homePresenter.test(HomeUiState.Loading) { @@ -111,6 +128,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) homePresenter.test(HomeUiState.Loading) { @@ -137,6 +155,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) homePresenter.test(HomeUiState.Loading) { @@ -162,9 +181,6 @@ internal class HomePresenterTest { showChatIcon = true, hasUnseenChatMessages = false, showHelpCenter = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, crossSells = CrossSellSheetData(testCrossSell, listOf(), null), firstVetSections = listOf(), addonBannerInfos = emptyList(), @@ -190,9 +206,7 @@ internal class HomePresenterTest { veryImportantMessages = listOf(), memberReminders = MemberReminders(), isHelpCenterEnabled = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, + quickActions = emptyList(), firstVetAction = null, crossSellsAction = HomeTopBarAction.CrossSellsAction( CrossSellSheetData(testCrossSell, listOf(), null), @@ -220,6 +234,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) val addonOnlyCrossSells = CrossSellSheetData(null, listOf(), testAddon) @@ -237,9 +252,6 @@ internal class HomePresenterTest { crossSells = addonOnlyCrossSells, firstVetSections = listOf(), showHelpCenter = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, addonBannerInfos = listOf(), draftClaim = null, ).right(), @@ -266,6 +278,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) homePresenter.test(HomeUiState.Loading) { @@ -284,9 +297,6 @@ internal class HomePresenterTest { crossSells = CrossSellSheetData(null, listOf(), null), firstVetSections = listOf(), showHelpCenter = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -301,9 +311,7 @@ internal class HomePresenterTest { connectPayment = null, ), isHelpCenterEnabled = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, + quickActions = emptyList(), hasUnseenChatMessages = false, chatAction = null, firstVetAction = null, @@ -326,6 +334,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) homePresenter.test(HomeUiState.Loading) { @@ -351,6 +360,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) homePresenter.test(HomeUiState.Loading) { @@ -367,9 +377,6 @@ internal class HomePresenterTest { showChatIcon = false, hasUnseenChatMessages = hasNotification, showHelpCenter = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, firstVetSections = listOf(), crossSells = CrossSellSheetData(null, listOf(), null), addonBannerInfos = emptyList(), @@ -393,6 +400,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) homePresenter.test(HomeUiState.Loading) { @@ -409,9 +417,6 @@ internal class HomePresenterTest { crossSells = CrossSellSheetData(null, listOf(), null), firstVetSections = listOf(), showHelpCenter = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -424,9 +429,7 @@ internal class HomePresenterTest { veryImportantMessages = listOf(), memberReminders = MemberReminders(), isHelpCenterEnabled = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, + quickActions = emptyList(), hasUnseenChatMessages = false, chatAction = null, firstVetAction = null, @@ -449,6 +452,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) val firstVet = FirstVetSection( buttonTitle = "ButtonTitle", @@ -472,9 +476,6 @@ internal class HomePresenterTest { firstVet, ), showHelpCenter = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -487,9 +488,7 @@ internal class HomePresenterTest { veryImportantMessages = listOf(), memberReminders = MemberReminders(), isHelpCenterEnabled = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, + quickActions = emptyList(), hasUnseenChatMessages = false, chatAction = null, firstVetAction = HomeTopBarAction.FirstVetAction(listOf(firstVet)), @@ -512,6 +511,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) val crossSell = CrossSell( id = "id", @@ -534,9 +534,6 @@ internal class HomePresenterTest { crossSells = CrossSellSheetData(testCrossSell, listOf(crossSell), null), firstVetSections = listOf(), showHelpCenter = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -549,9 +546,7 @@ internal class HomePresenterTest { veryImportantMessages = listOf(), memberReminders = MemberReminders(), isHelpCenterEnabled = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, + quickActions = emptyList(), hasUnseenChatMessages = false, chatAction = null, firstVetAction = null, @@ -582,6 +577,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) homePresenter.test(HomeUiState.Loading) { assertThat(awaitItem()).isEqualTo(HomeUiState.Loading) @@ -597,9 +593,6 @@ internal class HomePresenterTest { crossSells = CrossSellSheetData(null, emptyList(), null), firstVetSections = listOf(), showHelpCenter = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -612,9 +605,7 @@ internal class HomePresenterTest { veryImportantMessages = listOf(), memberReminders = MemberReminders(), isHelpCenterEnabled = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, + quickActions = emptyList(), hasUnseenChatMessages = false, chatAction = HomeTopBarAction.ChatAction, firstVetAction = null, @@ -637,6 +628,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) homePresenter.test(HomeUiState.Loading) { assertThat(awaitItem()).isEqualTo(HomeUiState.Loading) @@ -652,9 +644,6 @@ internal class HomePresenterTest { crossSells = CrossSellSheetData(null, emptyList(), null), firstVetSections = listOf(), showHelpCenter = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, addonBannerInfos = emptyList(), draftClaim = null, ).right(), @@ -667,9 +656,7 @@ internal class HomePresenterTest { veryImportantMessages = listOf(), memberReminders = MemberReminders(), isHelpCenterEnabled = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, + quickActions = emptyList(), hasUnseenChatMessages = false, chatAction = null, firstVetAction = null, @@ -692,6 +679,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) val otherCrossSell = CrossSell( id = "other", @@ -731,6 +719,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, deleteClaimIntentDraftUseCase, + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) homePresenter.test(HomeUiState.Loading) { assertThat(awaitItem()).isEqualTo(HomeUiState.Loading) @@ -759,6 +748,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) homePresenter.test(HomeUiState.Loading) { assertThat(awaitItem()).isEqualTo(HomeUiState.Loading) @@ -786,6 +776,7 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, deleteClaimIntentDraftUseCase, + FakeGetMemberQuickActionsUseCase(emptyList().right()), ) homePresenter.test(HomeUiState.Loading) { assertThat(awaitItem()).isEqualTo(HomeUiState.Loading) @@ -810,8 +801,14 @@ internal class HomePresenterTest { } @Test - fun `member action eligibility flags each propagate independently to the ui state`() = runTest { + fun `home shows the first three member quick actions with sick-abroad filtered out`() = runTest { val getHomeDataUseCase = TestGetHomeDataUseCase() + val quickActions = listOf( + editInsuranceMultiSelect, + changeAddressLink, + sickAbroadLink, + paymentsLink, + ) val homePresenter = HomePresenter( getHomeDataUseCase, SeenImportantMessagesStorageImpl(), @@ -819,23 +816,55 @@ internal class HomePresenterTest { ApplicationScope(backgroundScope), false, TestDeleteClaimIntentDraftUseCase(), + FakeGetMemberQuickActionsUseCase(quickActions.right()), ) homePresenter.test(HomeUiState.Loading) { assertThat(awaitItem()).isInstanceOf() - getHomeDataUseCase.responseTurbine.add( - someIrrelevantHomeDataInstance.copy( - isEditInsuranceEnabled = true, - isMovingEnabled = false, - isTravelCertificateEnabled = true, - ).right(), - ) + getHomeDataUseCase.responseTurbine.add(someIrrelevantHomeDataInstance.right()) val success = assertThat(awaitItem()).isInstanceOf() - success.prop(HomeUiState.Success::isEditInsuranceEnabled).isTrue() - success.prop(HomeUiState.Success::isMovingEnabled).isFalse() - success.prop(HomeUiState.Success::isTravelCertificateEnabled).isTrue() + success.prop(HomeUiState.Success::quickActions) + .isEqualTo(listOf(editInsuranceMultiSelect, changeAddressLink, paymentsLink)) } } + private val editInsuranceMultiSelect = QuickAction.MultiSelectExpandedLink( + titleRes = Res.string.HC_QUICK_ACTIONS_EDIT_INSURANCE_TITLE, + hintTextRes = Res.string.HC_QUICK_ACTIONS_EDIT_INSURANCE_SUBTITLE, + links = listOf( + QuickAction.StandaloneQuickLink( + titleRes = Res.string.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_TITLE, + hintTextRes = Res.string.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_SUBTITLE, + quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkChangeTier, + ), + ), + ) + private val changeAddressLink = QuickAction.StandaloneQuickLink( + titleRes = Res.string.HC_QUICK_ACTIONS_CHANGE_ADDRESS_TITLE, + hintTextRes = Res.string.HC_QUICK_ACTIONS_CHANGE_ADDRESS_SUBTITLE, + quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkChangeAddress, + ) + private val paymentsLink = QuickAction.StandaloneQuickLink( + titleRes = Res.string.HC_QUICK_ACTIONS_PAYMENTS_TITLE, + hintTextRes = Res.string.HC_QUICK_ACTIONS_PAYMENTS_SUBTITLE, + quickLinkDestination = QuickLinkDestination.OuterDestination.QuickLinkConnectPayment, + ) + private val sickAbroadLink = QuickAction.StandaloneQuickLink( + titleRes = Res.string.HC_QUICK_ACTIONS_SICK_ABROAD_TITLE, + hintTextRes = Res.string.HC_QUICK_ACTIONS_SICK_ABROAD_SUBTITLE, + quickLinkDestination = InnerHelpCenterDestination.QuickLinkSickAbroad( + DeflectData( + title = null, + infoText = null, + warningText = null, + partnersContainer = null, + partnersInfo = null, + content = DeflectData.InfoBlock("", ""), + faq = emptyList(), + buttonText = "", + ), + ), + ) + private val someIrrelevantHomeDataInstance: HomeData = HomeData( contractStatus = HomeData.ContractStatus.Active, claimStatusCardsData = null, @@ -844,9 +873,6 @@ internal class HomePresenterTest { showChatIcon = false, hasUnseenChatMessages = false, showHelpCenter = false, - isEditInsuranceEnabled = false, - isMovingEnabled = false, - isTravelCertificateEnabled = false, firstVetSections = listOf(), crossSells = CrossSellSheetData(null, emptyList(), null), addonBannerInfos = emptyList(), @@ -870,6 +896,12 @@ private class FakeCrossSellHomeNotificationService : CrossSellHomeNotificationSe } } +private class FakeGetMemberQuickActionsUseCase( + private val result: Either>, +) : GetMemberQuickActionsUseCase { + override suspend fun invoke(): Either> = result +} + private class TestDeleteClaimIntentDraftUseCase : DeleteClaimIntentDraftUseCase { val deletedIdsTurbine = Turbine() var result: Either = Unit.right() From d85b44ac0222d1d8fe075d2e065bb297d247b749 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Thu, 6 Aug 2026 10:48:44 +0200 Subject: [PATCH 23/23] member-quick-actions: harden toNavKey and tidy quick-link wiring - Narrow toNavKey() to QuickLinkDestination.OuterDestination so the two inner (feature-navigated) destinations cannot reach it: the compiler now forces every caller to handle FirstVet/SickAbroad itself, replacing the previous runtime error() guard. Callers switch on `is OuterDestination`. - Rename the Help Center use-case parameter to getMemberQuickActionsUseCase to match its type. - Route Home's FirstVet quick action through a single local helper instead of duplicating the FirstVetKey add in two places. --- .../android/app/navigation/HedvigEntryProvider.kt | 11 ++++++++++- .../feature/help/center/HelpCenterPresenter.kt | 6 +++--- .../feature/help/center/HelpCenterViewModel.kt | 4 ++-- .../feature/home/home/navigation/HomeEntries.kt | 8 ++++---- .../android/memberquickactions/QuickLinkNavKey.kt | 13 ++++--------- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt index 0823c83eda..af6b954353 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt @@ -59,6 +59,8 @@ import com.hedvig.android.feature.travelcertificate.navigation.TravelCertificate import com.hedvig.android.feature.travelcertificate.navigation.travelCertificateEntries import com.hedvig.android.language.LanguageService import com.hedvig.android.logger.logcat +import com.hedvig.android.memberquickactions.InnerHelpCenterDestination +import com.hedvig.android.memberquickactions.QuickLinkDestination import com.hedvig.android.memberquickactions.toNavKey import com.hedvig.android.navigation.activity.ExternalNavigator import com.hedvig.android.navigation.common.HedvigNavKey @@ -250,7 +252,14 @@ private fun EntryProviderScope.addHomeEntries( backstack.add(CoInsuredAddInfoKey(contractId, type)) }, navigateToHelpCenter = { backstack.add(HelpCenterKey) }, - navigateToQuickLink = { destination -> backstack.add(destination.toNavKey()) }, + navigateToQuickLink = { destination -> + when (destination) { + is QuickLinkDestination.OuterDestination -> backstack.add(destination.toNavKey()) + + // Inner destinations (FirstVet, SickAbroad) are handled by feature-home before reaching here. + is InnerHelpCenterDestination -> error("Inner quick-link destinations are routed by the feature") + } + }, navigateToClaimChat = { resumeClaim -> backstack.add( ClaimChatKey( diff --git a/app/feature/feature-help-center/src/commonMain/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 index 50ef0c0ab4..c32b7ec949 100644 --- a/app/feature/feature-help-center/src/commonMain/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 @@ -103,7 +103,7 @@ internal data class HelpCenterUiState( } internal class HelpCenterPresenter( - private val getQuickLinksUseCase: GetMemberQuickActionsUseCase, + private val getMemberQuickActionsUseCase: GetMemberQuickActionsUseCase, private val hasAnyActiveConversationUseCase: HasAnyActiveConversationUseCase, private val getHelpCenterFAQUseCase: GetHelpCenterFAQUseCase, private val getPuppyGuideUseCase: GetPuppyGuideUseCase, @@ -160,7 +160,7 @@ internal class HelpCenterPresenter( val key: HedvigNavKey = when (destination) { is InnerHelpCenterDestination.FirstVet -> FirstVetKey(destination.sections) is InnerHelpCenterDestination.QuickLinkSickAbroad -> EmergencyKey(destination.deflectData) - else -> destination.toNavKey() + is QuickLinkDestination.OuterDestination -> destination.toNavKey() } backstack.add(key) } @@ -176,7 +176,7 @@ internal class HelpCenterPresenter( quickLinksUiState = HelpCenterUiState.QuickLinkUiState.Loading } combine( - flow = flow { emit(getQuickLinksUseCase.invoke()) }, + flow = flow { emit(getMemberQuickActionsUseCase.invoke()) }, flow2 = flow { emit(getHelpCenterFAQUseCase.invoke()) }, flow3 = getPuppyGuideUseCase.invoke(), flow4 = featureManager.isFeatureEnabled(Feature.DISABLE_PUPPY_GUIDE), diff --git a/app/feature/feature-help-center/src/commonMain/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 index 7b62d8f316..4c46293b3b 100644 --- a/app/feature/feature-help-center/src/commonMain/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 @@ -14,7 +14,7 @@ import dev.zacsweers.metro.Inject @Inject @HedvigViewModel(ActivityRetainedScope::class) internal class HelpCenterViewModel( - getQuickLinksUseCase: GetMemberQuickActionsUseCase, + getMemberQuickActionsUseCase: GetMemberQuickActionsUseCase, hasAnyActiveConversationUseCase: HasAnyActiveConversationUseCase, getHelpCenterFAQUseCase: GetHelpCenterFAQUseCase, getPuppyGuideUseCase: GetPuppyGuideUseCase, @@ -31,7 +31,7 @@ internal class HelpCenterViewModel( puppyGuide = null, ), presenter = HelpCenterPresenter( - getQuickLinksUseCase = getQuickLinksUseCase, + getMemberQuickActionsUseCase = getMemberQuickActionsUseCase, hasAnyActiveConversationUseCase = hasAnyActiveConversationUseCase, getHelpCenterFAQUseCase = getHelpCenterFAQUseCase, featureManager = featureManager, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt index ce6e0c09a0..f3f8ef616e 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt @@ -14,6 +14,7 @@ import com.hedvig.android.navigation.common.HedvigNavKey import com.hedvig.android.navigation.compose.Backstack import com.hedvig.android.navigation.compose.NavSuiteSceneDecoratorStrategy import com.hedvig.android.navigation.compose.add +import com.hedvig.android.ui.emergency.FirstVetSection import dev.zacsweers.metrox.viewmodel.metroViewModel fun EntryProviderScope.homeEntries( @@ -38,6 +39,7 @@ fun EntryProviderScope.homeEntries( ) { entry(metadata = NavSuiteSceneDecoratorStrategy.showNavBar()) { val viewModel: HomeViewModel = metroViewModel() + val navigateToFirstVet: (List) -> Unit = { sections -> backstack.add(FirstVetKey(sections)) } HomeDestination( viewModel = viewModel, onNavigateToInbox = dropUnlessResumed { onNavigateToInbox() }, @@ -53,7 +55,7 @@ fun EntryProviderScope.homeEntries( navigateToQuickLink = dropUnlessResumed { destination -> // FirstVet lives inside feature-home; every other destination is routed by the caller. if (destination is InnerHelpCenterDestination.FirstVet) { - backstack.add(FirstVetKey(destination.sections)) + navigateToFirstVet(destination.sections) } else { navigateToQuickLink(destination) } @@ -61,9 +63,7 @@ fun EntryProviderScope.homeEntries( openUrl = openUrl, openCrossSellUrl = openCrossSellUrl, openAppSettings = openAppSettings, - navigateToFirstVet = dropUnlessResumed { sections -> - backstack.add(FirstVetKey(sections)) - }, + navigateToFirstVet = dropUnlessResumed { sections -> navigateToFirstVet(sections) }, navigateToContactInfo = dropUnlessResumed { navigateToContactInfo() }, diff --git a/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/QuickLinkNavKey.kt b/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/QuickLinkNavKey.kt index 95315dc900..12d5713301 100644 --- a/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/QuickLinkNavKey.kt +++ b/app/shared/member-quick-actions/src/commonMain/kotlin/com/hedvig/android/memberquickactions/QuickLinkNavKey.kt @@ -23,15 +23,10 @@ import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestinati import com.hedvig.android.memberquickactions.QuickLinkDestination.OuterDestination.QuickLinkTravelCertificate import com.hedvig.android.navigation.common.HedvigNavKey -fun QuickLinkDestination.toNavKey(): HedvigNavKey = when (this) { - is InnerHelpCenterDestination.FirstVet -> { - error("InnerHelpCenterDestination is navigated by the consuming feature, not via toNavKey()") - } - - is InnerHelpCenterDestination.QuickLinkSickAbroad -> { - error("InnerHelpCenterDestination is navigated by the consuming feature, not via toNavKey()") - } - +// Only OuterDestinations map to a shared nav key. InnerHelpCenterDestinations (FirstVet, SickAbroad) +// are navigated by the consuming feature, and narrowing the receiver makes that a compile-time rule: +// a caller cannot reach this mapping without first handling the inner cases itself. +fun QuickLinkDestination.OuterDestination.toNavKey(): HedvigNavKey = when (this) { QuickLinkChangeAddress -> { SelectContractForMovingKey(MovingSource.OTHER) }