Updated on 2026-08-14
This commit is contained in:
parent
54f0be3e6c
commit
587a31520d
12 changed files with 274 additions and 53 deletions
|
|
@ -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<Unit, RootDetectedWarningComponent> {
|
||||
override fun create(context: AppComponentContext, params: Unit): RootDetectedWarningComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<AppRoute>()
|
||||
|
||||
private val stack: Value<ChildStack<AppRoute, Child>> = 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) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
|
|
|
|||
|
|
@ -2017,7 +2017,7 @@
|
|||
<string name="yield_module_transaction_exit_subtitle">%1$s выведено из Aave</string>
|
||||
<string name="yield_module_transaction_initialize">Режим доходности инициализирован</string>
|
||||
<string name="yield_module_transaction_reactivate">Режим доходности реактивирован</string>
|
||||
<string name="yield_module_transaction_topup">Перевод средств в Aave</string>
|
||||
<string name="yield_module_transaction_topup">Перевод в Aave</string>
|
||||
<string name="yield_module_transaction_topup_subtitle">%1$s отправлено в Aave</string>
|
||||
<string name="yield_module_transaction_withdraw">Вывод из Aave</string>
|
||||
<string name="yield_module_transfer_mode_automatic">Автоматически</string>
|
||||
|
|
|
|||
|
|
@ -1091,6 +1091,8 @@
|
|||
<string name="reset_cards_dialog_next_device_description">Please reset the next device to continue</string>
|
||||
<string name="ring_promo_text">Ring owners get 3 commission-free swaps on Changelly until 15.11!</string>
|
||||
<string name="ring_promo_title">Swap With 0% Fees Now!</string>
|
||||
<string name="root_detected_warning_description">Devices with root access are considered less secure. Your data may be exposed to additional risks.</string>
|
||||
<string name="root_detected_warning_title">Root access detected</string>
|
||||
<string name="save_user_wallet_agreement_access_description">Log into the app and check your balance without scanning the card or ring</string>
|
||||
<string name="save_user_wallet_agreement_access_title">Access the app</string>
|
||||
<string name="save_user_wallet_agreement_allow_biometrics">Allow to use biometrics</string>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -56,4 +56,8 @@ interface SettingsRepository {
|
|||
suspend fun setGooglePayAvailability(value: Boolean)
|
||||
|
||||
suspend fun isGooglePayAvailability(): Boolean
|
||||
|
||||
suspend fun isRootDetectedWarningShown(): Boolean
|
||||
|
||||
suspend fun setRootDetectedWarningShown(value: Boolean)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue