From fb5dcabfb60ea2cb4257c5c759cfdf370ea8aaef Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Thu, 27 Aug 2026 16:34:57 +0200 Subject: [PATCH 1/3] chore: add import style rule to CLAUDE.md Import the type, never the namespace. An import may shorten a qualified reference only when the short name still says what it is to someone reading that line cold. Sealed subclasses and enum entries pass that test and stay allowed; around 1200 such imports already exist and they are the house style. Members reached through a receiver that carries the meaning do not: Res.string, Res.drawable and Clock.System are banned, since string.FOO and System.now() lose what Res.string.FOO and Clock.System.now() tell the reader. Also bans import-only changes to lines that are not otherwise being edited. PR #3100 carried 29 gratuitous new imports and ~60 rewritten call sites through a single screen refactor, which buried the real change under churn. --- CLAUDE.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 80f1cbf821..55130dbcf9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -480,6 +480,47 @@ Configuration in `.editorconfig`: - **Entry functions:** `{feature}Entries` - **Use cases:** `{Action}{Domain}UseCase` (e.g., `GetHomeDataUseCase`) +### Imports + +**Import the type, never the namespace.** An import may shorten a qualified reference only when the +short name still says what it is to someone reading that line cold, without scrolling to the import +list. Sealed subclasses, enum entries and other types pass that test. A member reached through a +receiver that carries the meaning does not. + +Always allowed (this is the house style, ~1200 such imports exist): + +```kotlin +import com.hedvig.android.feature.home.home.ui.HomeUiState.Success // `is Success ->` reads fine +import com.hedvig.android.design.system.hedvig.TooltipDefaults.BeakDirection.TopEnd +import kotlin.time.Duration.Companion.seconds // enables the `5.seconds` idiom +``` + +Never allowed, because the receiver is the meaning: + +```kotlin +import hedvig.resources.Res.string // ❌ `stringResource(string.FOO)` → use `Res.string.FOO` +import hedvig.resources.Res.drawable // ❌ `painterResource(drawable.x)` → use `Res.drawable.x` +import kotlin.time.Clock.System // ❌ `System.now()` → use `Clock.System.now()` +import ...hedvig.TooltipDefaults.defaultStyle // ❌ `defaultStyle` alone names nothing +``` + +`Res` and `Clock` are the two that come up most: 193 files import `hedvig.resources.Res` plainly and +that is the standard. `System.now()` additionally reads as `java.lang.System` to anyone skimming. + +**Separately: never make an import-only change to a line you are not otherwise editing.** Converting +existing `HomeEvent.RefreshData` call sites to a bare `RefreshData` (or the reverse) is a whole-file +rewrite disguised as a diff. It buries the real change under churn and makes review and `git blame` +worse for no behavioural gain. + +**Why:** both halves of this rule protect the reader. The first protects whoever reads the line +later, the second protects whoever reviews the PR now. PR #3100 was one screen refactor carrying 29 +gratuitous new imports and ~60 rewritten call sites, and the formatting noise overshadowed the +actual work. + +**How to apply:** if you are touching a line for a real reason, use the correct form. If you are not +touching it, leave its qualification exactly as it is. Import cleanups that are genuinely wanted go +in their own commit. + ### Comments Code comments and KDoc must describe the **current** code and stand on their own. A comment fails to earn its place in two ways: it tells the wrong kind of story, or it repeats what is already there. Before writing one, apply the test: *would this make sense to someone reading the file cold, with no knowledge of the PR, the conversation, or what was decided against?* If not, it does not belong in the source. Do not reference: From f4cfae8e9cd802ff846050b284a5f97fcbeb764a Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Thu, 27 Aug 2026 16:50:59 +0200 Subject: [PATCH 2/3] chore: qualify imports that hid their receiver Fixes every existing violation of the import rule: 26 imports across 12 files. Res.string.FOO and Clock.System.now() are restored at their use sites in the feature files that had shortened them away. Nineteen of the rest are design system Defaults members whose owning object lives in the same file, so they are now qualified as TooltipDefaults.defaultStyle and the like, matching what TopAppBar.kt already did. References from inside an owning object's own body are untouched, since they resolve without an import and gain nothing from the prefix. TopAppBar.kt's import turned out to be unused except for one call in TopAppBarLayoutForActions, which has no windowInsets parameter of its own and was silently reading the import. --- .../design/system/hedvig/ClickableList.kt | 3 +- .../android/design/system/hedvig/Dialog.kt | 5 +- .../design/system/hedvig/HedvigBigCard.kt | 15 ++-- .../design/system/hedvig/Notification.kt | 20 +++--- .../android/design/system/hedvig/Tooltip.kt | 14 ++-- .../android/design/system/hedvig/TopAppBar.kt | 3 +- .../system/hedvig/freetext/FreeTextOverlay.kt | 3 +- .../hedvig/freetext/FreeTextTriggerDisplay.kt | 12 ++-- .../details/ui/SubmittedAndClosedColumns.kt | 7 +- .../feature/home/home/ui/HomeDestination.kt | 68 +++++++++---------- .../ui/payments/PaymentsDestination.kt | 42 ++++++------ .../hedvig/android/crosssells/CrossSells.kt | 42 ++++++------ 12 files changed, 106 insertions(+), 128 deletions(-) diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ClickableList.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ClickableList.kt index 3a7b62da7a..7b20638907 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ClickableList.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/ClickableList.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp -import com.hedvig.android.design.system.hedvig.ClickableListDefaults.iconSize import com.hedvig.android.design.system.hedvig.icon.ChevronRight import com.hedvig.android.design.system.hedvig.icon.HedvigIcons import com.hedvig.android.design.system.hedvig.tokens.ColorSchemeKeyTokens @@ -96,7 +95,7 @@ private fun ClickableListItem( horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, ) { - Icon(HedvigIcons.ChevronRight, "", Modifier.size(iconSize)) + Icon(HedvigIcons.ChevronRight, "", Modifier.size(ClickableListDefaults.iconSize)) } }, spaceBetween = 4.dp, diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Dialog.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Dialog.kt index 080797e28b..6d226b271b 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Dialog.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Dialog.kt @@ -40,7 +40,6 @@ import com.hedvig.android.design.system.hedvig.DialogDefaults.ButtonSize.SMALL import com.hedvig.android.design.system.hedvig.DialogDefaults.DialogStyle import com.hedvig.android.design.system.hedvig.DialogDefaults.DialogStyle.Buttons import com.hedvig.android.design.system.hedvig.DialogDefaults.DialogStyle.NoButtons -import com.hedvig.android.design.system.hedvig.DialogDefaults.defaultButtonSize import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateButtonStyle import com.hedvig.android.design.system.hedvig.EmptyStateDefaults.EmptyStateIconStyle.ERROR import com.hedvig.android.design.system.hedvig.tokens.DialogTokens @@ -86,7 +85,7 @@ fun HedvigAlertDialog( modifier: Modifier = Modifier, confirmButtonLabel: String = stringResource(Res.string.GENERAL_YES), dismissButtonLabel: String = stringResource(Res.string.GENERAL_NO), - buttonSize: DialogDefaults.ButtonSize = defaultButtonSize, + buttonSize: DialogDefaults.ButtonSize = DialogDefaults.defaultButtonSize, ) { HedvigAlertDialog( title = AnnotatedString(title), @@ -109,7 +108,7 @@ fun HedvigAlertDialog( modifier: Modifier = Modifier, confirmButtonLabel: String = stringResource(Res.string.GENERAL_YES), dismissButtonLabel: String = stringResource(Res.string.GENERAL_NO), - buttonSize: DialogDefaults.ButtonSize = defaultButtonSize, + buttonSize: DialogDefaults.ButtonSize = DialogDefaults.defaultButtonSize, ) { HedvigDialog( style = Buttons( diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigBigCard.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigBigCard.kt index 02fc97e4ec..2e4a52c097 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigBigCard.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/HedvigBigCard.kt @@ -15,9 +15,6 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import com.hedvig.android.compose.ui.LayoutWithoutPlacement -import com.hedvig.android.design.system.hedvig.BigCardDefaults.inputTextStyle -import com.hedvig.android.design.system.hedvig.BigCardDefaults.labelTextStyle -import com.hedvig.android.design.system.hedvig.BigCardDefaults.padding import com.hedvig.android.design.system.hedvig.tokens.ColorSchemeKeyTokens import com.hedvig.android.design.system.hedvig.tokens.TypographyKeyTokens @@ -57,7 +54,7 @@ fun HedvigBigCard( inputText: String?, modifier: Modifier = Modifier, enabled: Boolean = true, - textStyle: TextStyle = inputTextStyle, + textStyle: TextStyle = BigCardDefaults.inputTextStyle, ) { Surface( shape = HedvigTheme.shapes.cornerLarge, @@ -72,14 +69,14 @@ fun HedvigBigCard( LayoutWithoutPlacement( sizeAdjustingContent = { // Always take up the space that the two texts would take - Column(Modifier.padding(padding)) { - HedvigText(text = labelText, style = labelTextStyle) + Column(Modifier.padding(BigCardDefaults.padding)) { + HedvigText(text = labelText, style = BigCardDefaults.labelTextStyle) HedvigText(text = "H", style = textStyle) } }, ) { if (inputText == null) { - Box(Modifier.padding(padding)) { + Box(Modifier.padding(BigCardDefaults.padding)) { HedvigText( text = labelText, style = textStyle, @@ -88,10 +85,10 @@ fun HedvigBigCard( ) } } else { - Column(Modifier.padding(padding)) { + Column(Modifier.padding(BigCardDefaults.padding)) { HedvigText( text = labelText, - style = labelTextStyle, + style = BigCardDefaults.labelTextStyle, color = bigCardColors.labelTextColor(enabled), ) HedvigText( diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt index 511ea7f4d8..a05e5a0e8c 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Notification.kt @@ -43,10 +43,6 @@ import com.hedvig.android.design.system.hedvig.NotificationDefaults.Notification import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority.Info import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority.InfoInline import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority.NeutralToast -import com.hedvig.android.design.system.hedvig.NotificationDefaults.defaultStyle -import com.hedvig.android.design.system.hedvig.NotificationDefaults.paddingNoIcon -import com.hedvig.android.design.system.hedvig.NotificationDefaults.paddingWithIcon -import com.hedvig.android.design.system.hedvig.NotificationDefaults.textStyle import com.hedvig.android.design.system.hedvig.icon.Campaign import com.hedvig.android.design.system.hedvig.icon.HedvigIcons import com.hedvig.android.design.system.hedvig.icon.InfoFilled @@ -82,7 +78,7 @@ fun HedvigNotificationCard( priority: NotificationPriority, modifier: Modifier = Modifier, withIcon: Boolean = NotificationDefaults.withIconDefault, - style: InfoCardStyle = defaultStyle, + style: InfoCardStyle = NotificationDefaults.defaultStyle, buttonLoading: Boolean = false, minLines: Int = 1, ) { @@ -104,10 +100,10 @@ fun HedvigNotificationCard( priority: NotificationPriority, modifier: Modifier = Modifier, withIcon: Boolean = NotificationDefaults.withIconDefault, - style: InfoCardStyle = defaultStyle, + style: InfoCardStyle = NotificationDefaults.defaultStyle, buttonLoading: Boolean = false, ) { - val padding = if (withIcon) paddingWithIcon else paddingNoIcon + val padding = if (withIcon) NotificationDefaults.paddingWithIcon else NotificationDefaults.paddingNoIcon val description = when (priority) { Attention, NotificationPriority.AttentionRound, Error, Info -> stringResource(Res.string.TALKBACK_NOTIFICATION_CARD) Campaign, InfoInline, NeutralToast, FancyInfo -> "" @@ -125,7 +121,7 @@ fun HedvigNotificationCard( border = if (priority !is FancyInfo) priority.colors.borderColor else null, ) { val buttonDarkTheme = if (priority is InfoInline) isSystemInDarkTheme() else false - ProvideTextStyle(textStyle) { + ProvideTextStyle(NotificationDefaults.textStyle) { Row(Modifier.padding(padding)) { if (withIcon) { LayoutWithoutPlacement( @@ -156,7 +152,7 @@ fun HedvigNotificationCard( buttonSize = Small, modifier = Modifier.weight(1f), ) { - HedvigText(style.leftButtonText, style = textStyle) + HedvigText(style.leftButtonText, style = NotificationDefaults.textStyle) } Spacer(Modifier.width(4.dp)) HedvigButton( @@ -166,7 +162,7 @@ fun HedvigNotificationCard( buttonSize = Small, modifier = Modifier.weight(1f), ) { - HedvigText(style.rightButtonText, style = textStyle) + HedvigText(style.rightButtonText, style = NotificationDefaults.textStyle) } } } @@ -184,11 +180,11 @@ fun HedvigNotificationCard( ) { LayoutWithoutPlacement( sizeAdjustingContent = { - HedvigText(style.buttonText, style = textStyle) + HedvigText(style.buttonText, style = NotificationDefaults.textStyle) }, ) { if (!buttonLoading) { - HedvigText(style.buttonText, style = textStyle) + HedvigText(style.buttonText, style = NotificationDefaults.textStyle) } else { Box( modifier = Modifier.fillMaxSize(), diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Tooltip.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Tooltip.kt index 51f9fe76ae..cfebcca170 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Tooltip.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Tooltip.kt @@ -49,10 +49,6 @@ import com.hedvig.android.design.system.hedvig.TooltipDefaults.TooltipStyle.Camp import com.hedvig.android.design.system.hedvig.TooltipDefaults.TooltipStyle.Campaign.Brightness.BLEAK import com.hedvig.android.design.system.hedvig.TooltipDefaults.TooltipStyle.Campaign.Brightness.BRIGHT import com.hedvig.android.design.system.hedvig.TooltipDefaults.TooltipStyle.Inbox -import com.hedvig.android.design.system.hedvig.TooltipDefaults.arrowHeightDp -import com.hedvig.android.design.system.hedvig.TooltipDefaults.arrowSpaceFromEdgeWhenOffCenteredDp -import com.hedvig.android.design.system.hedvig.TooltipDefaults.arrowWidthDp -import com.hedvig.android.design.system.hedvig.TooltipDefaults.defaultStyle import com.hedvig.android.design.system.hedvig.tokens.TooltipTokens import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.delay @@ -63,7 +59,7 @@ fun HedvigTooltip( showTooltip: Boolean, tooltipShown: () -> Unit, modifier: Modifier = Modifier, - tooltipStyle: TooltipStyle = defaultStyle, + tooltipStyle: TooltipStyle = TooltipDefaults.defaultStyle, beakDirection: BeakDirection = BottomCenter, maxWidth: Dp = TooltipDefaults.defaultMaxWidth, ) { @@ -150,9 +146,11 @@ private fun InnerChatTooltip( private fun Shape.withBeak(beakDirection: BeakDirection): Shape { return object : Shape { override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline { - val arrowSpaceFromEdgeWhenOffCentered: Float = with(density) { arrowSpaceFromEdgeWhenOffCenteredDp.toPx() } - val arrowWidth = with(density) { arrowWidthDp.toPx() } - val arrowHeight = with(density) { arrowHeightDp.toPx() } + val arrowSpaceFromEdgeWhenOffCentered: Float = with(density) { + TooltipDefaults.arrowSpaceFromEdgeWhenOffCenteredDp.toPx() + } + val arrowWidth = with(density) { TooltipDefaults.arrowWidthDp.toPx() } + val arrowHeight = with(density) { TooltipDefaults.arrowHeightDp.toPx() } val squircleSize: Size = when (beakDirection) { BottomCenter, BottomStart, BottomEnd, TopCenter, TopStart, TopEnd -> { size.copy(height = size.height - arrowHeight) diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/TopAppBar.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/TopAppBar.kt index fc624cdb91..e02e662b31 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/TopAppBar.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/TopAppBar.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.dropUnlessResumed -import com.hedvig.android.design.system.hedvig.TopAppBarDefaults.windowInsets import com.hedvig.android.design.system.hedvig.icon.ArrowLeft import com.hedvig.android.design.system.hedvig.icon.Close import com.hedvig.android.design.system.hedvig.icon.HedvigIcons @@ -183,7 +182,7 @@ fun TopAppBarLayoutForActions( horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End), verticalAlignment = Alignment.CenterVertically, modifier = modifier - .windowInsetsPadding(windowInsets) + .windowInsetsPadding(TopAppBarDefaults.windowInsets) .height(TopAppBarTokens.ContainerHeight) .fillMaxWidth() .padding(contentPadding), diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/freetext/FreeTextOverlay.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/freetext/FreeTextOverlay.kt index 8aca058ddc..a1fa4bec38 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/freetext/FreeTextOverlay.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/freetext/FreeTextOverlay.kt @@ -56,7 +56,6 @@ import com.hedvig.android.design.system.hedvig.HedvigText import com.hedvig.android.design.system.hedvig.HedvigTheme import com.hedvig.android.design.system.hedvig.HorizontalDivider import com.hedvig.android.design.system.hedvig.Surface -import com.hedvig.android.design.system.hedvig.freetext.FreeTextDefaults.counterPadding import com.hedvig.android.design.system.hedvig.fromToken import com.hedvig.android.design.system.hedvig.internal.Decoration import com.hedvig.android.design.system.hedvig.tokens.ColorSchemeKeyTokens.BackgroundBlack @@ -241,7 +240,7 @@ private fun FreeTextOverlayContent( color = freeTextColors.labelColor, modifier = Modifier .fillMaxWidth() - .padding(counterPadding) + .padding(FreeTextDefaults.counterPadding) .wrapContentWidth(Alignment.End) .semantics { contentDescription = characterLimitDescription diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/freetext/FreeTextTriggerDisplay.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/freetext/FreeTextTriggerDisplay.kt index dd2ad8a6bb..67b179696f 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/freetext/FreeTextTriggerDisplay.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/freetext/FreeTextTriggerDisplay.kt @@ -33,10 +33,6 @@ import com.hedvig.android.design.system.hedvig.freetext.FreeTextDisplayDefaults. import com.hedvig.android.design.system.hedvig.freetext.FreeTextDisplayDefaults.Height.Unlimited import com.hedvig.android.design.system.hedvig.freetext.FreeTextDisplayDefaults.Style import com.hedvig.android.design.system.hedvig.freetext.FreeTextDisplayDefaults.Style.Labeled -import com.hedvig.android.design.system.hedvig.freetext.FreeTextDisplayDefaults.contentPadding -import com.hedvig.android.design.system.hedvig.freetext.FreeTextDisplayDefaults.defaultHeight -import com.hedvig.android.design.system.hedvig.freetext.FreeTextDisplayDefaults.defaultStyle -import com.hedvig.android.design.system.hedvig.freetext.FreeTextDisplayDefaults.supportingTextPadding import com.hedvig.android.design.system.hedvig.fromToken import com.hedvig.android.design.system.hedvig.icon.HedvigIcons import com.hedvig.android.design.system.hedvig.icon.WarningFilled @@ -64,8 +60,8 @@ fun FreeTextDisplay( freeTextPlaceholder: String, modifier: Modifier = Modifier, maxLength: Int = FreeTextDisplayDefaults.maxLength, - height: Height = defaultHeight, - style: Style = defaultStyle, + height: Height = FreeTextDisplayDefaults.defaultHeight, + style: Style = FreeTextDisplayDefaults.defaultStyle, hasError: Boolean = false, supportingText: String? = null, showCount: Boolean = true, @@ -87,7 +83,7 @@ fun FreeTextDisplay( color = freeTextColors.displayContainerColor, ) { Column( - Modifier.padding(contentPadding), + Modifier.padding(FreeTextDisplayDefaults.contentPadding), ) { if (style is Labeled && freeTextValue != null) { Row(Modifier.fillMaxWidth()) { @@ -147,7 +143,7 @@ fun FreeTextDisplay( text = supportingText, color = displayColors.supportingTextColor, style = FreeTextDisplayDefaults.countLabelStyle.value, - modifier = Modifier.padding(supportingTextPadding), + modifier = Modifier.padding(FreeTextDisplayDefaults.supportingTextPadding), ) } } diff --git a/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/ui/SubmittedAndClosedColumns.kt b/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/ui/SubmittedAndClosedColumns.kt index 4f1ba9dc96..ee84d2e8e4 100644 --- a/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/ui/SubmittedAndClosedColumns.kt +++ b/app/feature/feature-claim-details/src/main/kotlin/com/hedvig/android/feature/claim/details/ui/SubmittedAndClosedColumns.kt @@ -21,7 +21,6 @@ import hedvig.resources.claim_status_detail_closed import hedvig.resources.claim_status_detail_submitted import java.util.Locale import kotlin.time.Clock -import kotlin.time.Clock.System import kotlin.time.Duration import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.seconds @@ -77,7 +76,7 @@ private fun SubmittedAndClosedColumn(topText: String, bottomText: String, modifi } @Composable -private fun currentTimeAsState(updateInterval: Duration = 1.seconds, clock: Clock = System): State { +private fun currentTimeAsState(updateInterval: Duration = 1.seconds, clock: Clock = Clock.System): State { return produceState(initialValue = clock.now()) { while (isActive) { delay(updateInterval) @@ -123,8 +122,8 @@ private fun PreviewSubmittedAndClosedInformation() { HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { SubmittedAndClosedColumns( - submittedAt = System.now().minus(10.days), - closedAt = System.now().minus(30.seconds), + submittedAt = Clock.System.now().minus(10.days), + closedAt = Clock.System.now().minus(30.seconds), locale = Locale.ENGLISH, ) } 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 1016a91ad5..63c923a5b4 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 @@ -224,8 +224,7 @@ import hedvig.resources.RESUME_CLAIM_DELETE_BUTTON import hedvig.resources.RESUME_CLAIM_DELETE_TITLE import hedvig.resources.RESUME_CLAIM_EXPIRED_BODY import hedvig.resources.RESUME_CLAIM_EXPIRED_TITLE -import hedvig.resources.Res.drawable -import hedvig.resources.Res.string +import hedvig.resources.Res import hedvig.resources.TOAST_NEW_OFFER import hedvig.resources.blur_background import hedvig.resources.general_cancel_button @@ -236,7 +235,6 @@ import hedvig.resources.home_tab_welcome_title_without_name import hedvig.resources.ongoing_shop_session_dismiss_offer import kotlin.math.roundToInt import kotlin.time.Clock -import kotlin.time.Clock.System import kotlin.time.Duration.Companion.milliseconds import kotlin.time.ExperimentalTime import kotlin.time.Instant @@ -387,8 +385,8 @@ private fun HomeScreen( // The draft is expired, so acknowledging the notice (Close button, scrim, or back) removes it. // Matches the Ready-for-dev design: single Close, closing removes the draft claim card. ErrorDialog( - title = stringResource(string.RESUME_CLAIM_EXPIRED_TITLE), - message = stringResource(string.RESUME_CLAIM_EXPIRED_BODY), + title = stringResource(Res.string.RESUME_CLAIM_EXPIRED_TITLE), + message = stringResource(Res.string.RESUME_CLAIM_EXPIRED_BODY), onDismiss = { showDraftExpiredDialog = false draftClaim?.let { deleteDraftClaim(it.id) } @@ -398,10 +396,10 @@ private fun HomeScreen( val draftIdToDelete = draftIdPendingDeleteConfirmation if (draftIdToDelete != null) { HedvigAlertDialog( - title = stringResource(string.RESUME_CLAIM_DELETE_TITLE), - text = stringResource(string.RESUME_CLAIM_DELETE_BODY), - confirmButtonLabel = stringResource(string.RESUME_CLAIM_DELETE_BUTTON), - dismissButtonLabel = stringResource(string.general_cancel_button), + title = stringResource(Res.string.RESUME_CLAIM_DELETE_TITLE), + text = stringResource(Res.string.RESUME_CLAIM_DELETE_BODY), + confirmButtonLabel = stringResource(Res.string.RESUME_CLAIM_DELETE_BUTTON), + dismissButtonLabel = stringResource(Res.string.general_cancel_button), onDismissRequest = { draftIdPendingDeleteConfirmation = null }, onConfirmClick = { draftIdPendingDeleteConfirmation = null @@ -456,7 +454,7 @@ private fun HomeScreen( }, onContinueDraftClaim = { if (draftClaim != null) { - if (draftClaim.isExpired(System.now())) { + if (draftClaim.isExpired(Clock.System.now())) { showDraftExpiredDialog = true } else { navigateToClaimChat(true) @@ -557,7 +555,7 @@ private fun HomeScreenTopBar( } if (shouldShowNewMessageTooltip) { HedvigTooltip( - message = stringResource(string.CHAT_NEW_MESSAGE), + message = stringResource(Res.string.CHAT_NEW_MESSAGE), showTooltip = shouldShowNewMessageTooltip, tooltipStyle = Inbox, beakDirection = TopEnd, @@ -586,7 +584,7 @@ private fun ColumnScope.CrossSellsTooltip(uiState: Success, setEpochDayWhenLastT var shouldSetEpochDayWhenLastToolTipShown by remember { mutableStateOf(false) } LaunchedEffect(shouldSetEpochDayWhenLastToolTipShown) { if (shouldSetEpochDayWhenLastToolTipShown) { - val today = System.now().toLocalDateTime( + val today = Clock.System.now().toLocalDateTime( TimeZone.currentSystemDefault(), ).date.toEpochDays() delay(5000.milliseconds) @@ -595,7 +593,7 @@ private fun ColumnScope.CrossSellsTooltip(uiState: Success, setEpochDayWhenLastT } if (shouldShowCrossSellsTooltip) { HedvigTooltip( - message = stringResource(string.TOAST_NEW_OFFER), + message = stringResource(Res.string.TOAST_NEW_OFFER), showTooltip = true, tooltipStyle = Campaign( subMessage = null, @@ -682,7 +680,7 @@ private fun HomeScreenSuccess( // to "hide" it (the content cards already do; so do the pinned pills). if (HedvigTheme.colorScheme.isLight) { Image( - painter = painterResource(drawable.blur_background), + painter = painterResource(Res.drawable.blur_background), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.matchParentSize(), @@ -1211,7 +1209,7 @@ private fun MemberRemindersSection( .padding(horizontalInsets), ) { HedvigText( - text = stringResource(string.HOME_TODO_SECTION_TITLE), + text = stringResource(Res.string.HOME_TODO_SECTION_TITLE), style = HedvigTheme.typography.headlineSmall, modifier = Modifier.semantics { heading() }, ) @@ -1243,7 +1241,7 @@ private fun QuotesSection( val contentPadding = PaddingValues(horizontal = 16.dp) + horizontalInsets Column(Modifier.fillMaxWidth()) { HedvigText( - text = stringResource(string.HOME_QUOTES_SECTION_TITLE), + text = stringResource(Res.string.HOME_QUOTES_SECTION_TITLE), style = HedvigTheme.typography.headlineSmall, modifier = Modifier .padding(contentPadding) @@ -1300,7 +1298,7 @@ private fun QuoteCard( HedvigText(text = session.title, style = HedvigTheme.typography.bodySmall) val secondary = session.monthlyNet?.let { stringResource( - string.OFFER_COST_AND_PREMIUM_PERIOD_ABBREVIATION, + Res.string.OFFER_COST_AND_PREMIUM_PERIOD_ABBREVIATION, it, ) } ?: session.subtitle @@ -1322,13 +1320,13 @@ private fun QuoteCard( ) { Icon( imageVector = HedvigIcons.Close, - contentDescription = stringResource(string.ongoing_shop_session_dismiss_offer), + contentDescription = stringResource(Res.string.ongoing_shop_session_dismiss_offer), ) } } Spacer(Modifier.height(12.dp)) HedvigButton( - text = stringResource(string.general_continue_button), + text = stringResource(Res.string.general_continue_button), onClick = { onResumeClick(session.resumeUrl) }, buttonStyle = Secondary, buttonSize = ButtonSize.Medium, @@ -1356,7 +1354,7 @@ private fun QuickActionTilesSection( .padding(horizontalInsets), ) { HedvigText( - text = stringResource(string.HC_QUICK_ACTIONS_TITLE), + text = stringResource(Res.string.HC_QUICK_ACTIONS_TITLE), style = HedvigTheme.typography.headlineSmall, modifier = Modifier.semantics { heading() }, ) @@ -1484,21 +1482,21 @@ private fun MainActionCarouselSection( .padding(horizontalInsets), ) { HedvigButton( - text = stringResource(string.home_tab_claim_button_text), + text = stringResource(Res.string.home_tab_claim_button_text), onClick = onMakeClaim, enabled = true, buttonStyle = RoundedPrimary, ) if (isHelpCenterEnabled) { HedvigButton( - text = stringResource(string.home_tab_get_help), + text = stringResource(Res.string.home_tab_get_help), onClick = onHelpAndSupport, enabled = true, buttonStyle = RoundedLiquidGlass, ) } HedvigButton( - text = stringResource(string.DASHBOARD_OPEN_CHAT), + text = stringResource(Res.string.DASHBOARD_OPEN_CHAT), onClick = onContactUs, enabled = true, buttonStyle = RoundedLiquidGlass, @@ -1521,7 +1519,7 @@ private fun AddonsSection( .padding(horizontalInsets), ) { HedvigText( - text = stringResource(string.INSURANCE_ADDONS_SUBHEADING), + text = stringResource(Res.string.INSURANCE_ADDONS_SUBHEADING), style = HedvigTheme.typography.headlineSmall, modifier = Modifier.semantics { heading() }, ) @@ -1531,7 +1529,7 @@ private fun AddonsSection( subtitle = addon.description, pillowImage = null, pillow = { AddonPillow(addon.flowType) }, - buttonText = stringResource(string.HOME_ADDONS_READ_MORE_BUTTON), + buttonText = stringResource(Res.string.HOME_ADDONS_READ_MORE_BUTTON), onButtonClick = { navigateToAddonPurchaseFlow(addon.eligibleInsurancesIds) }, imageLoader = imageLoader, modifier = Modifier.fillMaxWidth(), @@ -1549,7 +1547,7 @@ private fun DiscoverInsurancesSection( imageLoader: ImageLoader, ) { CrossSellsSection( - title = stringResource(string.HOME_DISCOVER_SECTION_TITLE), + title = stringResource(Res.string.HOME_DISCOVER_SECTION_TITLE), crossSells = crossSells, onCrossSellClick = onCrossSellClick, modifier = Modifier.padding(horizontal = 16.dp), @@ -1571,7 +1569,7 @@ private fun WelcomeMessage(firstName: String, modifier: Modifier = Modifier) { ) if (firstName.isBlank()) { HedvigText( - text = stringResource(string.home_tab_welcome_title_without_name), + text = stringResource(Res.string.home_tab_welcome_title_without_name), style = titleStyle, modifier = modifier.fillMaxWidth(), ) @@ -1582,12 +1580,12 @@ private fun WelcomeMessage(firstName: String, modifier: Modifier = Modifier) { modifier = modifier.fillMaxWidth(), ) { HedvigText( - text = stringResource(string.HOME_GREETING_TITLE, firstName), + text = stringResource(Res.string.HOME_GREETING_TITLE, firstName), style = titleStyle, modifier = Modifier.fillMaxWidth(), ) HedvigText( - text = stringResource(string.HOME_GREETING_SUBTITLE), + text = stringResource(Res.string.HOME_GREETING_SUBTITLE), color = HedvigTheme.colorScheme.textSecondary, style = titleStyle, modifier = Modifier.fillMaxWidth(), @@ -1879,19 +1877,19 @@ private fun PreviewHomeScreenAllHomeTextTypes( private val previewQuickActions: List = listOf( MultiSelectExpandedLink( - titleRes = string.HC_QUICK_ACTIONS_EDIT_INSURANCE_TITLE, - hintTextRes = string.HC_QUICK_ACTIONS_EDIT_INSURANCE_SUBTITLE, + titleRes = Res.string.HC_QUICK_ACTIONS_EDIT_INSURANCE_TITLE, + hintTextRes = Res.string.HC_QUICK_ACTIONS_EDIT_INSURANCE_SUBTITLE, links = listOf( StandaloneQuickLink( - titleRes = string.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_TITLE, - hintTextRes = string.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_SUBTITLE, + titleRes = Res.string.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_TITLE, + hintTextRes = Res.string.HC_QUICK_ACTIONS_UPGRADE_COVERAGE_SUBTITLE, quickLinkDestination = QuickLinkChangeTier, ), ), ), StandaloneQuickLink( - titleRes = string.HC_QUICK_ACTIONS_CHANGE_ADDRESS_TITLE, - hintTextRes = string.HC_QUICK_ACTIONS_CHANGE_ADDRESS_SUBTITLE, + titleRes = Res.string.HC_QUICK_ACTIONS_CHANGE_ADDRESS_TITLE, + hintTextRes = Res.string.HC_QUICK_ACTIONS_CHANGE_ADDRESS_SUBTITLE, quickLinkDestination = QuickLinkChangeAddress, ), ) diff --git a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt index 5d674040e5..f2c7a75f37 100644 --- a/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt +++ b/app/feature/feature-payments/src/main/kotlin/com/hedvig/android/feature/payments/ui/payments/PaymentsDestination.kt @@ -117,7 +117,7 @@ import hedvig.resources.Res import hedvig.resources.TAB_PAYMENTS_TITLE import hedvig.resources.info_card_missing_payment_body import hedvig.resources.info_card_missing_payment_missing_payments_body -import kotlin.time.Clock.System +import kotlin.time.Clock import kotlin.time.Duration.Companion.days import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone @@ -822,7 +822,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< isRetrying = false, upcomingPayment = UpcomingPayment.Content( UiMoney(100.0, SEK), - System.now().toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().toLocalDateTime(TimeZone.UTC).date, "rdg", ), upcomingPaymentInfo = NoInfo, @@ -837,7 +837,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< isRetrying = false, upcomingPayment = UpcomingPayment.Content( UiMoney(100.0, SEK), - System.now().toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().toLocalDateTime(TimeZone.UTC).date, "rdg", ), upcomingPaymentInfo = NoInfo, @@ -854,7 +854,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< isRetrying = false, upcomingPayment = UpcomingPayment.Content( UiMoney(100.0, SEK), - System.now().toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().toLocalDateTime(TimeZone.UTC).date, "iky", ), upcomingPaymentInfo = InProgress, @@ -869,12 +869,12 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< isRetrying = false, upcomingPayment = UpcomingPayment.Content( UiMoney(400.0, SEK), - System.now().toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().toLocalDateTime(TimeZone.UTC).date, "pwe", ), upcomingPaymentInfo = PaymentFailed( - System.now().toLocalDateTime(TimeZone.UTC).date, - System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, isManualChargeAllowed = ManualChargeToPrompt( UiMoney(200.0, UiCurrencyCode.SEK), ), @@ -890,7 +890,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< isRetrying = false, upcomingPayment = UpcomingPayment.Content( UiMoney(100.0, SEK), - System.now().toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().toLocalDateTime(TimeZone.UTC).date, "fkjse", ), upcomingPaymentInfo = NoInfo, @@ -905,12 +905,12 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< isRetrying = false, upcomingPayment = UpcomingPayment.Content( UiMoney(100.0, SEK), - System.now().toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().toLocalDateTime(TimeZone.UTC).date, "qrdfgeth", ), upcomingPaymentInfo = PaymentFailed( - System.now().toLocalDateTime(TimeZone.UTC).date, - System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, isManualChargeAllowed = null, ), ongoingCharges = emptyList(), @@ -926,7 +926,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< isRetrying = false, upcomingPayment = UpcomingPayment.Content( UiMoney(100.0, SEK), - System.now().toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().toLocalDateTime(TimeZone.UTC).date, "qrdfgeth2", ), upcomingPaymentInfo = NoInfo, @@ -943,17 +943,17 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< isRetrying = false, upcomingPayment = UpcomingPayment.Content( UiMoney(100.0, SEK), - System.now().toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().toLocalDateTime(TimeZone.UTC).date, "w345423t6", ), upcomingPaymentInfo = PaymentFailed( - System.now().toLocalDateTime(TimeZone.UTC).date, - System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, isManualChargeAllowed = null, ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.NeedsPayinSetup( - dueDateToConnect = System.now().plus(30.days).toLocalDateTime(TimeZone.UTC).date, + dueDateToConnect = Clock.System.now().plus(30.days).toLocalDateTime(TimeZone.UTC).date, ), showPayoutButton = false, memberType = MemberType.STANDARD_MEMBER, @@ -964,17 +964,17 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< isRetrying = false, upcomingPayment = UpcomingPayment.Content( UiMoney(100.0, SEK), - System.now().toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().toLocalDateTime(TimeZone.UTC).date, "42345", ), upcomingPaymentInfo = PaymentFailed( - System.now().toLocalDateTime(TimeZone.UTC).date, - System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().minus(30.days).toLocalDateTime(TimeZone.UTC).date, isManualChargeAllowed = null, ), ongoingCharges = emptyList(), connectedPaymentInfo = ConnectedPaymentInfo.NeedsPayinSetup( - System.now().plus(30.days).toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().plus(30.days).toLocalDateTime(TimeZone.UTC).date, ), showPayoutButton = false, memberType = MemberType.STANDARD_MEMBER, @@ -1029,7 +1029,7 @@ private class PaymentsStatePreviewProvider : CollectionPreviewParameterProvider< isRetrying = false, upcomingPayment = UpcomingPayment.Content( UiMoney(100.0, SEK), - System.now().toLocalDateTime(TimeZone.UTC).date, + Clock.System.now().toLocalDateTime(TimeZone.UTC).date, "w345423t6", ), upcomingPaymentInfo = UpcomingPaymentInfo.NoInfo, 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 992007a488..1056d4a6af 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 @@ -87,8 +87,6 @@ import hedvig.resources.CROSS_SELL_BANNER_TEXT import hedvig.resources.CROSS_SELL_SUBTITLE import hedvig.resources.CROSS_SELL_TITLE import hedvig.resources.Res -import hedvig.resources.Res.plurals -import hedvig.resources.Res.string import hedvig.resources.TALKBACK_OPEN_EXTERNAL_LINK import hedvig.resources.cross_sell_get_price import hedvig.resources.general_close_button @@ -147,7 +145,7 @@ fun CrossSellFloatingBottomSheet( dragHandle = { CrossSellDragHandle( text = state.data?.recommendedCrossSell?.bannerText - ?: state.data?.recommendedAddon?.let { it.bannerText ?: stringResource(string.CROSS_SELL_BANNER_TEXT) }, + ?: state.data?.recommendedAddon?.let { it.bannerText ?: stringResource(Res.string.CROSS_SELL_BANNER_TEXT) }, modifier = Modifier .padding(horizontal = 16.dp) .clip(HedvigTheme.shapes.cornerXLargeTop), @@ -186,7 +184,7 @@ fun CrossSellBottomSheet( contentPadding = PaddingValues(horizontal = 16.dp), text = state.data?.recommendedCrossSell?.bannerText ?: state.data?.recommendedAddon?.bannerText - ?: stringResource(string.CROSS_SELL_BANNER_TEXT), + ?: stringResource(Res.string.CROSS_SELL_BANNER_TEXT), ) } } else { @@ -247,7 +245,7 @@ private fun CrossSellsSheetContent( if (otherCrossSells.isNotEmpty()) { Column { Spacer(Modifier.height(24.dp)) - HedvigText(stringResource(string.CROSS_SELL_SUBTITLE), Modifier.semantics { heading() }) + HedvigText(stringResource(Res.string.CROSS_SELL_SUBTITLE), Modifier.semantics { heading() }) Spacer(Modifier.height(24.dp)) CrossSellsSection( crossSells = otherCrossSells, @@ -260,7 +258,7 @@ private fun CrossSellsSheetContent( } } HedvigButton( - text = stringResource(string.general_close_button), + text = stringResource(Res.string.general_close_button), onClick = dismissSheet, enabled = true, buttonStyle = ButtonStyle.Ghost, @@ -318,7 +316,7 @@ private fun CrossSellsFloatingSheetContent( if (otherCrossSells.isNotEmpty()) { Column { Spacer(Modifier.height(24.dp)) - HedvigText(stringResource(string.CROSS_SELL_SUBTITLE), Modifier.semantics { heading() }) + HedvigText(stringResource(Res.string.CROSS_SELL_SUBTITLE), Modifier.semantics { heading() }) Spacer(Modifier.height(24.dp)) CrossSellsSection( crossSells = otherCrossSells, @@ -336,7 +334,7 @@ private fun CrossSellsFloatingSheetContent( shape = HedvigTheme.shapes.cornerLarge, ) { HedvigButton( - text = stringResource(string.general_close_button), + text = stringResource(Res.string.general_close_button), onClick = dismissSheet, enabled = true, buttonStyle = ButtonStyle.Secondary, @@ -398,7 +396,7 @@ private fun AddonRecommendationSection( } } Spacer(Modifier.height(24.dp)) - val headingDescription = stringResource(string.CROSS_SELL_TITLE) + + val headingDescription = stringResource(Res.string.CROSS_SELL_TITLE) + ": ${recommendedAddon.title}" HedvigText( text = recommendedAddon.title, @@ -466,7 +464,7 @@ private fun RecommendationSection( ) { StackedPillows(recommendedCrossSell, imageLoader) Spacer(Modifier.height(24.dp)) - val headingDescription = stringResource(string.CROSS_SELL_TITLE) + + val headingDescription = stringResource(Res.string.CROSS_SELL_TITLE) + ": ${recommendedCrossSell.crossSell.title}" HedvigText( text = recommendedCrossSell.crossSell.title, @@ -492,7 +490,7 @@ private fun RecommendationSection( stepProgressItems.joinToString(separator = "; ") { item -> "${item.title} - ${item.subtitle}" } val description = "$dataDescription; " + pluralStringResource( - plurals.A11Y_NUMBER_OF_ELIGIBLE_INSURANCES, + Res.plurals.A11Y_NUMBER_OF_ELIGIBLE_INSURANCES, recommendedCrossSell.bundleProgress.numberOfEligibleContracts, recommendedCrossSell.bundleProgress.numberOfEligibleContracts, ) @@ -511,7 +509,7 @@ private fun RecommendationSection( onCrossSellClick(recommendedCrossSell.crossSell.storeUrl) dismissSheet() }, - onClickLabel = stringResource(string.TALKBACK_OPEN_EXTERNAL_LINK), + onClickLabel = stringResource(Res.string.TALKBACK_OPEN_EXTERNAL_LINK), enabled = true, modifier = Modifier .fillMaxWidth() @@ -594,16 +592,16 @@ private fun StackedPillows(recommendedCrossSell: RecommendedCrossSell, imageLoad @Composable private fun getHedvigStepProgressData(bundleProgress: BundleProgress): List { - val firstStepTitle = stringResource(string.BUNDLE_DISCOUNT_PROGRESS_SEGMENT_TITLE_ONE_INSURANCE) - val firstStepSubtitle = stringResource(string.BUNDLE_DISCOUNT_PROGRESS_SEGMENT_SUBTITLE_NO_DISCOUNT) + val firstStepTitle = stringResource(Res.string.BUNDLE_DISCOUNT_PROGRESS_SEGMENT_TITLE_ONE_INSURANCE) + val firstStepSubtitle = stringResource(Res.string.BUNDLE_DISCOUNT_PROGRESS_SEGMENT_SUBTITLE_NO_DISCOUNT) val stepOne = StepProgressItem(firstStepTitle, firstStepSubtitle) - val secondStepTitle = stringResource(string.BUNDLE_DISCOUNT_PROGRESS_SEGMENT_TITLE_TWO_INSURANCES) + val secondStepTitle = stringResource(Res.string.BUNDLE_DISCOUNT_PROGRESS_SEGMENT_TITLE_TWO_INSURANCES) val secondStepSubtitle = stringResource( - string.BUNDLE_DISCOUNT_PROGRESS_SEGMENT_SUBTITLE_CURRENT_APPLIED_DISCOUNT, + Res.string.BUNDLE_DISCOUNT_PROGRESS_SEGMENT_SUBTITLE_CURRENT_APPLIED_DISCOUNT, "${bundleProgress.discountPercent}%", ) val stepTwo = StepProgressItem(secondStepTitle, secondStepSubtitle) - val thirdStepTitle = stringResource(string.BUNDLE_DISCOUNT_PROGRESS_SEGMENT_TITLE_THREE_OR_MORE) + val thirdStepTitle = stringResource(Res.string.BUNDLE_DISCOUNT_PROGRESS_SEGMENT_TITLE_THREE_OR_MORE) val stepThree = StepProgressItem(thirdStepTitle, secondStepSubtitle) return listOf(stepOne, stepTwo, stepThree) } @@ -680,7 +678,7 @@ fun CrossSellItemPlaceholder(imageLoader: ImageLoader, modifier: Modifier = Modi private fun CrossSellsSubHeaderWithDivider(title: String? = null) { Column { NotificationSubheading( - text = title ?: stringResource(string.insurance_tab_cross_sells_title), + text = title ?: stringResource(Res.string.insurance_tab_cross_sells_title), modifier = Modifier.semantics { heading() }, ) Spacer(Modifier.height(16.dp)) @@ -738,7 +736,7 @@ private fun CrossSellItem( onSheetDismissed() }, imageLoader = imageLoader, - onButtonClickLabel = stringResource(string.TALKBACK_OPEN_EXTERNAL_LINK), + onButtonClickLabel = stringResource(Res.string.TALKBACK_OPEN_EXTERNAL_LINK), isLoading = isLoading, modifier = modifier, buttonSize = buttonSize, @@ -905,12 +903,12 @@ private fun CrossSellItemWithDiscounts( } Spacer(Modifier.width(16.dp)) HedvigButton( - text = buttonText ?: stringResource(string.cross_sell_get_price), + text = buttonText ?: stringResource(Res.string.cross_sell_get_price), onClick = { onCrossSellClick(storeUrl) onSheetDismissed() }, - onClickLabel = stringResource(string.TALKBACK_OPEN_EXTERNAL_LINK), + onClickLabel = stringResource(Res.string.TALKBACK_OPEN_EXTERNAL_LINK), buttonSize = buttonSize, buttonStyle = ButtonStyle.PrimaryAlt, shape = buttonShape, @@ -938,7 +936,7 @@ private fun NotificationSubheading(text: String, modifier: Modifier = Modifier) private fun CrossSellDragHandle( modifier: Modifier = Modifier, contentPadding: PaddingValues? = null, - text: String? = stringResource(string.CROSS_SELL_BANNER_TEXT), + text: String? = stringResource(Res.string.CROSS_SELL_BANNER_TEXT), ) { val direction = LocalLayoutDirection.current Box( From b3d63131b09be9dc0fa99df05f976bcb7b232968 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 28 Aug 2026 14:44:45 +0200 Subject: [PATCH 3/3] chore: add a ktlint namespace-import rule kotlinter runs ktlint over every source set, so a custom ruleset enforces the import rule everywhere: androidMain, commonMain, iosMain and plain JVM modules alike. Android Lint would be the more precise tool, since it resolves types, but it cannot reach KMP modules at all, and 19 of the violations just cleaned up live in one. AGP's com.android.kotlin.multiplatform.library registers a lint {} DSL but no task that runs it, and it sits outside com.android.base, so plugin logic keying off the Android plugins does nothing there. Applying the standalone com.android.lint plugin alongside it does produce a working lint that reads commonMain, but every generateAndroidMainLintModel then depends on itself and the build fails with a circular dependency the moment two such modules depend on each other. Neither checkDependencies=false nor android.experimental.lint.analysisPerComponent=false avoids it, and com.android.internal.lint only ever analyses the JVM components, which do not include commonMain. Google issue 246751841 has tracked this since 2022. Having no type resolution means this cannot ask whether an owner is a class or a package. CAPITALIZED_PACKAGES carries that cost: Kotlin/Native interop packages are named after the framework they bind, so platform.Foundation.systemLocale is shaped exactly like a member import and has to be excluded by prefix. --- .../src/main/kotlin/HedvigGradlePlugin.kt | 8 +++ gradle/libs.versions.toml | 4 ++ hedvig-ktlint/build.gradle.kts | 9 +++ .../android/ktlint/HedvigRuleSetProvider.kt | 13 ++++ .../android/ktlint/NamespaceImportRule.kt | 66 +++++++++++++++++++ ...int.cli.ruleset.core.api.RuleSetProviderV3 | 1 + settings.gradle.kts | 1 + 7 files changed, 102 insertions(+) create mode 100644 hedvig-ktlint/build.gradle.kts create mode 100644 hedvig-ktlint/src/main/kotlin/com/hedvig/android/ktlint/HedvigRuleSetProvider.kt create mode 100644 hedvig-ktlint/src/main/kotlin/com/hedvig/android/ktlint/NamespaceImportRule.kt create mode 100644 hedvig-ktlint/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 diff --git a/build-logic/convention/src/main/kotlin/HedvigGradlePlugin.kt b/build-logic/convention/src/main/kotlin/HedvigGradlePlugin.kt index d5e62a119d..773d36609e 100644 --- a/build-logic/convention/src/main/kotlin/HedvigGradlePlugin.kt +++ b/build-logic/convention/src/main/kotlin/HedvigGradlePlugin.kt @@ -38,6 +38,14 @@ private fun Project.configureKtlint(libs: LibrariesForLibs) { reporters = arrayOf(ReporterType.checkstyle.name) } + // Our own rules run on every source set, which is what gives KMP modules the coverage that + // Android Lint cannot reach. + if (name != "hedvig-ktlint") { + dependencies { + add("ktlint", project(":hedvig-ktlint")) + } + } + tasks.withType().configureEach { exclude { it.file.path.contains("generated/") } reports.set( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cc345f31c8..ef82d6a592 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,8 @@ kmpNativeCoroutines = "1.0.5" kotlin = "2.4.10" kotlinpoet = "2.3.0" kotlinter = "5.6.0" +# Must match the ktlint that kotlinter bundles, so the custom ruleset links against the same API +ktlintCore = "1.8.0" ksp = "2.3.10" ktor = "3.5.1" license = "0.9.9" @@ -270,6 +272,8 @@ kmpNativeCoroutines-gradlePlugin = { module = "com.rickclephas.kmp.nativecorouti kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlinSerialization-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } kotlinter-gradlePlugin = { module = "org.jmailen.gradle:kotlinter-gradle", version.ref = "kotlinter" } +ktlint-ruleEngineCore = { module = "com.pinterest.ktlint:ktlint-rule-engine-core", version.ref = "ktlintCore" } +ktlint-cliRulesetCore = { module = "com.pinterest.ktlint:ktlint-cli-ruleset-core", version.ref = "ktlintCore" } ksp-gradlePlugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } license-gradlePlugin = { module = "com.jaredsburrows.license:com.jaredsburrows.license.gradle.plugin", version.ref = "license" } metro-gradlePlugin = { module = "dev.zacsweers.metro:dev.zacsweers.metro.gradle.plugin", version.ref = "metro" } diff --git a/hedvig-ktlint/build.gradle.kts b/hedvig-ktlint/build.gradle.kts new file mode 100644 index 0000000000..d22714ecab --- /dev/null +++ b/hedvig-ktlint/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("hedvig.jvm.library") + id("hedvig.gradle.plugin") +} + +dependencies { + compileOnly(libs.ktlint.ruleEngineCore) + compileOnly(libs.ktlint.cliRulesetCore) +} diff --git a/hedvig-ktlint/src/main/kotlin/com/hedvig/android/ktlint/HedvigRuleSetProvider.kt b/hedvig-ktlint/src/main/kotlin/com/hedvig/android/ktlint/HedvigRuleSetProvider.kt new file mode 100644 index 0000000000..9e599aab53 --- /dev/null +++ b/hedvig-ktlint/src/main/kotlin/com/hedvig/android/ktlint/HedvigRuleSetProvider.kt @@ -0,0 +1,13 @@ +package com.hedvig.android.ktlint + +import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 +import com.pinterest.ktlint.rule.engine.core.api.RuleProvider +import com.pinterest.ktlint.rule.engine.core.api.RuleSetId + +internal const val CUSTOM_RULE_SET_ID = "hedvig" + +class HedvigRuleSetProvider : RuleSetProviderV3(RuleSetId(CUSTOM_RULE_SET_ID)) { + override fun getRuleProviders(): Set = setOf( + RuleProvider { NamespaceImportRule() }, + ) +} diff --git a/hedvig-ktlint/src/main/kotlin/com/hedvig/android/ktlint/NamespaceImportRule.kt b/hedvig-ktlint/src/main/kotlin/com/hedvig/android/ktlint/NamespaceImportRule.kt new file mode 100644 index 0000000000..55940827b8 --- /dev/null +++ b/hedvig-ktlint/src/main/kotlin/com/hedvig/android/ktlint/NamespaceImportRule.kt @@ -0,0 +1,66 @@ +package com.hedvig.android.ktlint + +import com.pinterest.ktlint.rule.engine.core.api.ElementType +import com.pinterest.ktlint.rule.engine.core.api.Rule +import com.pinterest.ktlint.rule.engine.core.api.RuleId +import org.jetbrains.kotlin.com.intellij.lang.ASTNode + +/** + * Reports imports that shorten a qualified reference past the point where the short name still says + * what it is, such as `import hedvig.resources.Res.string` turning `Res.string.FOO` into `string.FOO`. + * + * ktlint has no type resolution, so an owner is recognized by the shape of the import path rather + * than by resolving it, and [CAPITALIZED_PACKAGES] carries the exceptions that costs us. + */ +internal class NamespaceImportRule : + Rule( + ruleId = RuleId("$CUSTOM_RULE_SET_ID:namespace-import"), + about = About( + maintainer = "Hedvig", + repositoryUrl = "https://github.com/HedvigInsurance/android", + issueTrackerUrl = "https://github.com/HedvigInsurance/android/issues", + ), + ) { + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + if (node.elementType != ElementType.IMPORT_DIRECTIVE) return + val text = node.text + // An alias is a deliberate act of renaming, and gives the use site a name of its own. + if (text.contains(" as ")) return + val qualifiedName = text.removePrefix("import").trim() + if (qualifiedName.isEmpty() || qualifiedName.endsWith("*")) return + + val importedName = qualifiedName.substringAfterLast('.') + val ownerPath = qualifiedName.substringBeforeLast('.', "") + val ownerName = ownerPath.substringAfterLast('.') + if (importedName.isEmpty() || ownerName.isEmpty()) return + if (!ownerName.first().isUpperCase()) return + // `Duration.Companion.seconds` and friends exist to enable the `5.seconds` receiver idiom. + if (ownerName == "Companion") return + if (CAPITALIZED_PACKAGES.any { qualifiedName.startsWith(it) }) return + + val importsAMember = importedName.first().isLowerCase() + if (!importsAMember && qualifiedName !in DENIED_IMPORTS) return + + emit( + node.startOffset, + "Import $ownerName and write $ownerName.$importedName at the use site. " + + "On its own, $importedName no longer says what it is.", + false, + ) + } + + private companion object { + /** + * Kotlin/Native interop packages are capitalized after the framework they bind, so their + * top-level declarations look identical to members of a class. + */ + val CAPITALIZED_PACKAGES = listOf("platform.") + + /** Imports that read as a type but still leave nothing meaningful at the use site. */ + val DENIED_IMPORTS = setOf("kotlin.time.Clock.System") + } +} diff --git a/hedvig-ktlint/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 b/hedvig-ktlint/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 new file mode 100644 index 0000000000..61cf4aff39 --- /dev/null +++ b/hedvig-ktlint/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 @@ -0,0 +1 @@ +com.hedvig.android.ktlint.HedvigRuleSetProvider diff --git a/settings.gradle.kts b/settings.gradle.kts index 0c0abfeb19..cfe8b6f178 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -69,3 +69,4 @@ include("design-showcase-desktop") project(":design-showcase-desktop").projectDir = rootProject.projectDir.resolve("micro-apps").resolve("design-showcase-desktop") include("hedvig-lint") +include("hedvig-ktlint")