From 587a31520daa8c4ba29be45d481252c9ffadf94b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Dec 2025 13:35:11 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../root/RootDetectedWarningComponent.kt | 68 ++++++++++++++ .../root/RootDetectedWarningContent.kt | 93 +++++++++++++++++++ .../com/tangem/tap/routing/RootContent.kt | 3 + .../component/impl/DefaultRoutingComponent.kt | 9 ++ .../local/preferences/PreferencesKeys.kt | 2 + core/res/src/main/res/values-ru/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 2 + .../core/ui/components/DialogFullScreen.kt | 70 +++++++------- .../message/MessageBottomSheetV2.kt | 24 ++--- .../ui/components/icons/HighlightedIcon.kt | 39 ++++++++ .../settings/DefaultSettingsRepository.kt | 11 +++ .../repositories/SettingsRepository.kt | 4 + 12 files changed, 274 insertions(+), 53 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt create mode 100644 app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt diff --git a/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt new file mode 100644 index 0000000000..16077bce72 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningComponent.kt @@ -0,0 +1,68 @@ +package com.tangem.tap.features.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.essenty.instancekeeper.getOrCreateSimple +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.components.DialogFullScreen +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.security.isSecurityExposed +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +@Suppress("UnusedPrivateProperty") +class RootDetectedWarningComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Unit, + private val securityInfoProvider: DeviceSecurityInfoProvider, + private val settingsRepository: SettingsRepository, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val isShown = instanceKeeper.getOrCreateSimple { MutableStateFlow(false) } + + suspend fun tryToShowWarningAndWaitContinuation() { + if (isShown.value) return + + if (settingsRepository.isRootDetectedWarningShown().not() && securityInfoProvider.isSecurityExposed()) { + isShown.value = true + } + + isShown.first { it == false } // Wait until the warning is dismissed + } + + @Composable + override fun Content(modifier: Modifier) { + val isShownState by isShown.collectAsStateWithLifecycle() + + if (isShownState) { + DialogFullScreen(onDismissRequest = {}) { + RootDetectedWarningContent( + modifier = modifier, + onContinueClick = remember(this) { ::onContinueClick }, + ) + } + } + } + + private fun onContinueClick() { + componentScope.launch { + settingsRepository.setRootDetectedWarningShown(true) + isShown.value = false + } + } + + @AssistedFactory + interface Factory : ComponentFactory { + override fun create(context: AppComponentContext, params: Unit): RootDetectedWarningComponent + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt new file mode 100644 index 0000000000..9dc01d8a13 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/root/RootDetectedWarningContent.kt @@ -0,0 +1,93 @@ +package com.tangem.tap.features.root + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.icons.HighlightedIcon +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.wallet.R + +@Composable +internal fun RootDetectedWarningContent(modifier: Modifier = Modifier, onContinueClick: () -> Unit = {}) { + Column( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary) + .statusBarsPadding() + .padding(horizontal = 16.dp), + ) { + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center, + ) { + InfoBlock( + modifier = Modifier.padding(top = 48.dp, bottom = 24.dp), + ) + } + + PrimaryButton( + modifier = Modifier + .navigationBarsPadding() + .padding(bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.common_understand_continue), + onClick = onContinueClick, + ) + } +} + +@Composable +private fun InfoBlock(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + HighlightedIcon( + icon = R.drawable.ic_alert_circle_24, + iconTint = TangemTheme.colors.icon.warning, + ) + + SpacerH(20.dp) + + Text( + text = stringResourceSafe(R.string.root_detected_warning_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + + SpacerH(12.dp) + + Text( + modifier = Modifier.padding(horizontal = 24.dp), + text = stringResourceSafe(R.string.root_detected_warning_description), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + } +} + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + RootDetectedWarningContent() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/RootContent.kt b/app/src/main/java/com/tangem/tap/routing/RootContent.kt index add9354694..c21f7d2617 100644 --- a/app/src/main/java/com/tangem/tap/routing/RootContent.kt +++ b/app/src/main/java/com/tangem/tap/routing/RootContent.kt @@ -47,6 +47,7 @@ internal fun RootContent( modifier: Modifier = Modifier, wcContent: @Composable (modifier: Modifier) -> Unit, hotAccessCodeContent: @Composable (modifier: Modifier) -> Unit, + rootDetectedWarningContent: @Composable (modifier: Modifier) -> Unit, ) { val context = LocalContext.current @@ -82,6 +83,8 @@ internal fun RootContent( hotAccessCodeContent(Modifier.fillMaxSize()) + rootDetectedWarningContent(Modifier.fillMaxSize()) + TangemSnackbarHost( modifier = Modifier .align(Alignment.BottomCenter) diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 65ece0eaad..23bc630cec 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -38,6 +38,7 @@ import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.hot.TangemHotSDKProxy import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog +import com.tangem.tap.features.root.RootDetectedWarningComponent import com.tangem.tap.routing.RootContent import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.component.RoutingComponent.Child @@ -64,6 +65,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val tangemHotSDKProxy: TangemHotSDKProxy, private val hotAccessCodeRequestComponentFactory: HotAccessCodeRequestComponent.Factory, private val hotAccessCodeRequesterProxy: HotWalletPasswordRequesterProxy, + private val rootDetectedWarningComponentFactory: RootDetectedWarningComponent.Factory, private val userWalletsListRepository: UserWalletsListRepository, private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, @@ -85,6 +87,11 @@ internal class DefaultRoutingComponent @AssistedInject constructor( .create(child("hotAccessCodeRequestComponent"), Unit) } + private val rootDetectedWarningComponent: RootDetectedWarningComponent by lazy { + rootDetectedWarningComponentFactory + .create(child("rootDetectedWarningComponent"), Unit) + } + private val navigation = navigationProvider.getOrCreateTyped() private val stack: Value> = childStack( @@ -134,6 +141,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private fun initializeInitialNavigation() { if (initialStack.isNullOrEmpty()) { componentScope.launch { + rootDetectedWarningComponent.tryToShowWarningAndWaitContinuation() val initialRoute = resolveInitialRoute() router.replaceAll(initialRoute) } @@ -177,6 +185,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( modifier = modifier, wcContent = { wcRoutingComponent.Content(it) }, hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) }, + rootDetectedWarningContent = { rootDetectedWarningComponent.Content(it) }, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index b52614222b..78e99e42bd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -27,6 +27,8 @@ object PreferencesKeys { val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") } + val ROOT_DETECTED_WARNING_SHOWN_KEY by lazy { booleanPreferencesKey(name = "rootDetectedWarningShown") } + val SHOULD_SHOW_ASK_BIOMETRY_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") } val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") } diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 0d5eaa197f..03ce372d0f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -2017,7 +2017,7 @@ %1$s выведено из Aave Режим доходности инициализирован Режим доходности реактивирован - Перевод средств в Aave + Перевод в Aave %1$s отправлено в Aave Вывод из Aave Автоматически diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 524e2ec97f..07f6bd26c8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1091,6 +1091,8 @@ Please reset the next device to continue Ring owners get 3 commission-free swaps on Changelly until 15.11! Swap With 0% Fees Now! + Devices with root access are considered less secure. Your data may be exposed to additional risks. + Root access detected Log into the app and check your balance without scanning the card or ring Access the app Allow to use biometrics diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt index 6f70db0ba1..44e48c0fd0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/DialogFullScreen.kt @@ -37,44 +37,46 @@ fun DialogFullScreen( decorFitsSystemWindows = false, ), content = { - val activityWindow = getActivityWindow() - val dialogWindow = getDialogWindow() - val parentView = LocalView.current.parent as View - SideEffect { - if (activityWindow != null && dialogWindow != null) { - val attributes = WindowManager.LayoutParams().apply { - copyFrom(activityWindow.attributes) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE - } else { - flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS - } - type = dialogWindow.attributes.type - } - - dialogWindow.attributes = attributes - parentView.layoutParams = - FrameLayout.LayoutParams( - activityWindow.decorView.width, - activityWindow.decorView.height, - ) - } - } - - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - val systemUiController = rememberSystemUiController(getActivityWindow()) - val dialogSystemUiController = rememberSystemUiController(getDialogWindow()) - + ProvideSystemBarsIconsController { + val activityWindow = getActivityWindow() + val dialogWindow = getDialogWindow() + val parentView = LocalView.current.parent as View SideEffect { - systemUiController.setSystemBarsColor(color = Color.Transparent) - dialogSystemUiController.setSystemBarsColor(color = Color.Transparent) + if (activityWindow != null && dialogWindow != null) { + val attributes = WindowManager.LayoutParams().apply { + copyFrom(activityWindow.attributes) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + } else { + flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + } + type = dialogWindow.attributes.type + } + + dialogWindow.attributes = attributes + parentView.layoutParams = + FrameLayout.LayoutParams( + activityWindow.decorView.width, + activityWindow.decorView.height, + ) + } } - } - SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not()) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + val systemUiController = rememberSystemUiController(getActivityWindow()) + val dialogSystemUiController = rememberSystemUiController(getDialogWindow()) - Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) { - content() + SideEffect { + systemUiController.setSystemBarsColor(color = Color.Transparent) + dialogSystemUiController.setSystemBarsColor(color = Color.Transparent) + } + } + + SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not()) + + Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) { + content() + } } }, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt index f31c547290..cb3f2c538d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt @@ -2,17 +2,13 @@ package com.tangem.core.ui.components.bottomsheets.message import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -25,6 +21,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.components.icons.HighlightedIcon import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -144,20 +141,11 @@ private fun BottomSheetIcon(icon: MessageBottomSheetUMV2.Icon, modifier: Modifie MessageBottomSheetUMV2.Icon.BackgroundType.Warning -> TangemTheme.colors.icon.warning } - Box( - modifier = modifier - .size(TangemTheme.dimens.size56) - .clip(CircleShape) - .background(backgroundColor.copy(alpha = 0.1F)), - contentAlignment = Alignment.Center, - content = { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size32), - painter = painterResource(icon.res), - contentDescription = null, - tint = tint, - ) - }, + HighlightedIcon( + modifier = modifier, + icon = icon.res, + iconTint = tint, + backgroundColor = backgroundColor, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt new file mode 100644 index 0000000000..a94578a5ff --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/icons/HighlightedIcon.kt @@ -0,0 +1,39 @@ +package com.tangem.core.ui.components.icons + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun HighlightedIcon( + @DrawableRes icon: Int, + iconTint: Color, + modifier: Modifier = Modifier, + backgroundColor: Color = iconTint, +) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size56) + .clip(CircleShape) + .background(backgroundColor.copy(alpha = 0.1F)), + contentAlignment = Alignment.Center, + content = { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size32), + painter = painterResource(icon), + contentDescription = null, + tint = iconTint, + ) + }, + ) +} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index dea79f4d3a..588d3b71e3 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -193,4 +193,15 @@ internal class DefaultSettingsRepository( default = false, ) } + + override suspend fun isRootDetectedWarningShown(): Boolean { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.ROOT_DETECTED_WARNING_SHOWN_KEY, + default = false, + ) + } + + override suspend fun setRootDetectedWarningShown(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.ROOT_DETECTED_WARNING_SHOWN_KEY, value = value) + } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 0a8f854790..68ffcaf957 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -56,4 +56,8 @@ interface SettingsRepository { suspend fun setGooglePayAvailability(value: Boolean) suspend fun isGooglePayAvailability(): Boolean + + suspend fun isRootDetectedWarningShown(): Boolean + + suspend fun setRootDetectedWarningShown(value: Boolean) } \ No newline at end of file