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: 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( 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")