Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-27 13:22:43 +04:00
parent 1a0eaa779a
commit 654b2ff948
17 changed files with 103 additions and 36 deletions

View file

@ -15,7 +15,6 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -30,10 +29,7 @@ import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.text.applyBladeBrush import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
@ -218,7 +214,7 @@ fun getBalanceValueAndFlickerState(balance: UserWalletItemUM.Balance): Pair<Stri
is UserWalletItemUM.Balance.Failed -> DASH_SIGN to false is UserWalletItemUM.Balance.Failed -> DASH_SIGN to false
is UserWalletItemUM.Balance.Hidden -> THREE_STARS to false is UserWalletItemUM.Balance.Hidden -> THREE_STARS to false
is UserWalletItemUM.Balance.Loading -> null to false is UserWalletItemUM.Balance.Loading -> null to false
is UserWalletItemUM.Balance.Locked -> stringResource(R.string.common_locked) to false is UserWalletItemUM.Balance.Locked -> stringResourceSafe(R.string.common_locked) to false
is UserWalletItemUM.Balance.Loaded -> balance.value to balance.isFlickering is UserWalletItemUM.Balance.Loaded -> balance.value to balance.isFlickering
} }
} }

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.local.nft.converter package com.tangem.datasource.local.nft.converter
import android.content.res.Resources import android.content.res.Resources
import com.tangem.core.res.getStringSafe
import com.tangem.datasource.R import com.tangem.datasource.R
import com.tangem.domain.models.StatusSource import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.Network
@ -48,12 +49,12 @@ class NFTSdkCollectionConverter(
is NFTCollection.Identifier.EVM -> is NFTCollection.Identifier.EVM ->
name.toCollectionName() name.toCollectionName()
is NFTCollection.Identifier.TON -> if (collectionId.contractAddress == null) { is NFTCollection.Identifier.TON -> if (collectionId.contractAddress == null) {
resources.getString(R.string.nft_no_collection) resources.getStringSafe(R.string.nft_no_collection)
} else { } else {
name.toCollectionName() name.toCollectionName()
} }
is NFTCollection.Identifier.Solana -> if (collectionId.collectionAddress == null) { is NFTCollection.Identifier.Solana -> if (collectionId.collectionAddress == null) {
resources.getString(R.string.nft_no_collection) resources.getStringSafe(R.string.nft_no_collection)
} else { } else {
name.toCollectionName() name.toCollectionName()
} }
@ -61,7 +62,7 @@ class NFTSdkCollectionConverter(
} }
private fun String?.toCollectionName() = if (this.isNullOrEmpty()) { private fun String?.toCollectionName() = if (this.isNullOrEmpty()) {
resources.getString(R.string.nft_untitled_collection) resources.getStringSafe(R.string.nft_untitled_collection)
} else { } else {
this this
} }

View file

@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButton
@ -40,14 +39,14 @@ internal fun NFTCollectionsEmpty(state: NFTCollectionsUM.Empty, modifier: Modifi
) )
Text( Text(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing24), modifier = Modifier.padding(top = TangemTheme.dimens.spacing24),
text = stringResource(R.string.nft_collections_empty_title), text = stringResourceSafe(R.string.nft_collections_empty_title),
style = TangemTheme.typography.h3, style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1, color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
Text( Text(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
text = stringResource(R.string.nft_collections_empty_description), text = stringResourceSafe(R.string.nft_collections_empty_description),
style = TangemTheme.typography.body1, style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary, color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,

View file

@ -7,7 +7,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -43,7 +42,8 @@ internal fun OnboardingNoteCreateWallet(state: OnboardingNoteCreateWalletUM, mod
state = WalletArtworksState.Folded, state = WalletArtworksState.Folded,
) )
Column( Column(
modifier = Modifier.weight(1 - ALL_STEPS_TOP_CONTAINER_WEIGHT) modifier = Modifier
.weight(1 - ALL_STEPS_TOP_CONTAINER_WEIGHT)
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = 32.dp), .padding(horizontal = 32.dp),
@ -51,7 +51,7 @@ internal fun OnboardingNoteCreateWallet(state: OnboardingNoteCreateWalletUM, mod
) { ) {
SpacerHMax() SpacerHMax()
Text( Text(
text = stringResource(R.string.onboarding_create_wallet_header), text = stringResourceSafe(R.string.onboarding_create_wallet_header),
style = TangemTheme.typography.h2, style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1, color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -59,7 +59,7 @@ internal fun OnboardingNoteCreateWallet(state: OnboardingNoteCreateWalletUM, mod
) )
SpacerH16() SpacerH16()
Text( Text(
text = stringResource(R.string.onboarding_create_wallet_body), text = stringResourceSafe(R.string.onboarding_create_wallet_body),
style = TangemTheme.typography.body1, style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary, color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,

View file

@ -10,7 +10,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -134,7 +133,7 @@ private fun Content(
} else { } else {
state.onAccessCodeFirstChange state.onAccessCodeFirstChange
}, },
label = stringResource(id = R.string.onboarding_wallet_info_title_third), label = stringResourceSafe(id = R.string.onboarding_wallet_info_title_third),
isError = state.codesNotMatchError || state.atLeastMinCharsError, isError = state.codesNotMatchError || state.atLeastMinCharsError,
visualTransformation = if (state.accessCodeHidden) { visualTransformation = if (state.accessCodeHidden) {
PasswordVisualTransformation() PasswordVisualTransformation()
@ -143,9 +142,9 @@ private fun Content(
}, },
caption = when { caption = when {
state.codesNotMatchError && reEnterAccessCodeState -> state.codesNotMatchError && reEnterAccessCodeState ->
stringResource(R.string.onboarding_access_codes_doesnt_match) stringResourceSafe(R.string.onboarding_access_codes_doesnt_match)
state.atLeastMinCharsError && !reEnterAccessCodeState -> state.atLeastMinCharsError && !reEnterAccessCodeState ->
stringResource(R.string.onboarding_access_code_too_short) stringResourceSafe(R.string.onboarding_access_code_too_short)
else -> null else -> null
}, },
) )

View file

@ -8,12 +8,12 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.extensions.appendColored import com.tangem.core.ui.extensions.appendColored
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
@ -35,7 +35,7 @@ internal fun OnrampButtonComponent(state: OnrampMainComponentUM) {
OnrampTosText(providerState) OnrampTosText(providerState)
PrimaryButton( PrimaryButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.common_buy), text = stringResourceSafe(id = R.string.common_buy),
onClick = state.buyButtonConfig.onClick, onClick = state.buyButtonConfig.onClick,
enabled = state.buyButtonConfig.enabled, enabled = state.buyButtonConfig.enabled,
) )
@ -44,9 +44,9 @@ internal fun OnrampButtonComponent(state: OnrampMainComponentUM) {
@Composable @Composable
private fun OnrampTosText(provider: OnrampProviderBlockUM.Content?) { private fun OnrampTosText(provider: OnrampProviderBlockUM.Content?) {
val termsOfUse = stringResource(R.string.common_terms_of_use) val termsOfUse = stringResourceSafe(R.string.common_terms_of_use)
val privacyPolicy = stringResource(R.string.common_privacy_policy) val privacyPolicy = stringResourceSafe(R.string.common_privacy_policy)
val tosText = stringResource(R.string.onramp_legal, termsOfUse, privacyPolicy) val tosText = stringResourceSafe(R.string.onramp_legal, termsOfUse, privacyPolicy)
val clickableAnnotation = buildAnnotatedString { val clickableAnnotation = buildAnnotatedString {
append(tosText.substringBefore(termsOfUse)) append(tosText.substringBefore(termsOfUse))

View file

@ -10,19 +10,19 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.extensions.resourceReference
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.divider.DividerWithPadding import com.tangem.core.ui.components.divider.DividerWithPadding
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.impl.R
@ -75,7 +75,7 @@ internal fun WcEstimatedWalletChangesItem(item: WcEstimatedWalletChangesUM, modi
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = stringResource(if (isExpanded) R.string.common_show_less else R.string.common_show_more), text = stringResourceSafe(if (isExpanded) R.string.common_show_less else R.string.common_show_more),
style = TangemTheme.typography.body1, style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary, color = TangemTheme.colors.text.tertiary,
) )

View file

@ -191,6 +191,7 @@ firebase-perf = { module = "com.google.firebase:firebase-perf" }
# region Detekt # region Detekt
detekt-compose = { module = "ru.kode:detekt-rules-compose", version.ref = "detektComposeRules" } detekt-compose = { module = "ru.kode:detekt-rules-compose", version.ref = "detektComposeRules" }
detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
detekt-api = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" }
# endregion Detekt # endregion Detekt
# region Test # region Test

View file

@ -22,13 +22,19 @@ private fun DetektExtension.configure(project: Project) {
ignoreFailures = false ignoreFailures = false
autoCorrect = true autoCorrect = true
buildUponDefaultConfig = true buildUponDefaultConfig = true
config.setFrom(project.rootProject.files("tangem-android-tools/detekt-config.yml")) config.setFrom(
project.rootProject.files(
"tangem-android-tools/detekt-config.yml",
"plugins/detekt-rules/app-detekt-config.yml"
)
)
} }
private fun Project.configureDetektPlugins() { private fun Project.configureDetektPlugins() {
listOf( listOfNotNull(
findLibrary(alias = "detekt-formatting"), findLibrary(alias = "detekt-formatting"),
findLibrary(alias = "detekt-compose"), findLibrary(alias = "detekt-compose"),
findProject(":plugins:detekt-rules"),
).forEach { ).forEach {
dependencies.add(CONFIGURATION_DETEKT_PLUGINS, it) dependencies.add(CONFIGURATION_DETEKT_PLUGINS, it)
} }

1
plugins/detekt-rules/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,3 @@
tangem-rules:
UnsafeStringResourceUsage:
active: true

View file

@ -0,0 +1,7 @@
plugins {
alias(deps.plugins.kotlin.jvm)
}
dependencies {
implementation(deps.detekt.api)
}

View file

@ -0,0 +1,20 @@
package com.tangem.detekt
import com.tangem.detekt.rules.UnsafeStringResourceUsage
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.RuleSet
import io.gitlab.arturbosch.detekt.api.RuleSetProvider
class TangemRuleSetProvider : RuleSetProvider {
override val ruleSetId: String = "tangem-rules"
override fun instance(config: Config): RuleSet {
return RuleSet(
id = ruleSetId,
rules = listOf(
UnsafeStringResourceUsage(config),
)
)
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.detekt.rules
import io.gitlab.arturbosch.detekt.api.*
import org.jetbrains.kotlin.psi.*
class UnsafeStringResourceUsage(config: Config) : Rule(config) {
override val issue: Issue = Issue(
id = "UnsafeStringResourceUsage",
severity = Severity.Security,
description = "Avoid using stringResource directly in the code.",
debt = Debt.FIVE_MINS,
)
val unsafeFunctionNames = listOf("stringResource", "pluralStringResource")
override fun visitCallExpression(expression: KtCallExpression) {
super.visitCallExpression(expression)
val functionName = expression.calleeExpression?.text
if (functionName in unsafeFunctionNames) {
report(
CodeSmell(
issue = issue,
entity = Entity.from(expression),
message = "Usage of `$functionName` is unsafe. Use the version of the function with the `Safe` " +
"suffix. For example, `${functionName}Safe`."
)
)
}
}
}

View file

@ -0,0 +1 @@
com.tangem.detekt.TangemRuleSetProvider

View file

@ -134,6 +134,7 @@ dependencyResolutionManagement {
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
include(":app") include(":app")
include(":plugins:detekt-rules")
// region Core modules // region Core modules
include(":core:analytics") include(":core:analytics")

@ -1 +1 @@
Subproject commit 4eb84b4150ab0b03400707a6f121d4fd313e3cef Subproject commit 53e359bc2ead37feeb9e70d7004c91cde07ccf9b