Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-17 12:02:09 +05:00
parent 9bcf93922c
commit 4197574fca
10 changed files with 466 additions and 18 deletions

View file

@ -110,6 +110,7 @@ internal class ChildFactory @Inject constructor(
private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory,
private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory,
private val viewPhraseComponentFactory: ViewPhraseComponent.Factory,
private val forgetWalletComponentFactory: ForgetWalletComponent.Factory,
private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory,
private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory,
private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory,
@ -565,6 +566,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = viewPhraseComponentFactory,
)
}
is AppRoute.ForgetWallet -> {
createComponentChild(
context = context,
params = ForgetWalletComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = forgetWalletComponentFactory,
)
}
is AppRoute.SendEntryPoint -> {
createComponentChild(
context = context,

View file

@ -359,6 +359,11 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/view_seed_phrase/${userWalletId.stringValue}")
@Serializable
data class ForgetWallet(
val userWalletId: UserWalletId,
) : AppRoute(path = "/forget_wallet/${userWalletId.stringValue}")
@Serializable
data class SendEntryPoint(
val userWalletId: UserWalletId,

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="72"
android:viewportHeight="72">
<path
android:pathData="M69.781,45.692L69.79,45.709C71.126,48.06 72,50.879 72,53.877C72,57.927 70.612,62.075 67.529,65.265C64.394,68.508 60.145,70.096 55.73,70.096H16.27C11.855,70.096 7.606,68.508 4.471,65.265C1.388,62.075 0,57.927 0,53.877C0,51.094 0.713,48.238 2.266,45.611L21.966,11.287C25.02,5.881 30.514,3.086 36,3.086C41.553,3.086 46.97,5.934 50.018,11.26C50.02,11.263 50.022,11.266 50.023,11.269L69.781,45.692ZM41.932,15.893L61.688,50.313C62.324,51.433 62.68,52.681 62.68,53.877C62.68,57.772 60.058,60.777 55.73,60.777H16.27C11.942,60.777 9.32,57.772 9.32,53.877C9.32,52.681 9.625,51.459 10.312,50.313L30.068,15.893C31.367,13.577 33.658,12.406 36,12.406C38.342,12.406 40.608,13.577 41.932,15.893Z"
android:strokeAlpha="0.12"
android:fillColor="#FF3333"
android:fillType="evenOdd"
android:fillAlpha="0.12"/>
<path
android:pathData="M36,9.771C39.182,9.771 42.273,11.382 44.047,14.484L44.049,14.486L63.49,48.36C64.308,49.799 64.8,51.456 64.8,53.126C64.8,55.617 63.955,57.976 62.262,59.728C60.554,61.494 58.159,62.462 55.414,62.462H16.586C13.842,62.462 11.446,61.494 9.738,59.728C8.045,57.976 7.2,55.617 7.2,53.126C7.2,51.514 7.613,49.864 8.526,48.331L27.948,14.493C29.705,11.368 32.836,9.771 36,9.771ZM36,12.318C33.695,12.318 31.441,13.471 30.163,15.751L10.725,49.618C10.048,50.745 9.747,51.949 9.747,53.126C9.747,56.958 12.328,59.914 16.586,59.914H55.414C59.673,59.914 62.253,56.958 62.253,53.126C62.253,51.949 61.902,50.72 61.275,49.618L41.837,15.751C40.534,13.471 38.305,12.318 36,12.318ZM34.984,45.657C35.942,45.657 36.718,46.433 36.718,47.391C36.718,48.348 35.942,49.124 34.984,49.124C34.027,49.124 33.251,48.348 33.251,47.391C33.251,46.433 34.027,45.657 34.984,45.657ZM34.984,25.845C35.805,25.845 36.471,26.51 36.471,27.331V42.189C36.471,43.01 35.805,43.676 34.984,43.676C34.164,43.676 33.499,43.01 33.499,42.189V27.331C33.499,26.511 34.164,25.845 34.984,25.845Z"
android:fillColor="#FF3333"/>
</vector>

View file

@ -0,0 +1,14 @@
package com.tangem.features.hotwallet
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
interface ForgetWalletComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
)
interface Factory : ComponentFactory<Params, ForgetWalletComponent>
}

View file

@ -0,0 +1,39 @@
package com.tangem.features.hotwallet.forgetwallet
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.hotwallet.ForgetWalletComponent
import com.tangem.features.hotwallet.forgetwallet.ui.ForgetWalletContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember")
internal class DefaultForgetWalletComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: ForgetWalletComponent.Params,
) : ForgetWalletComponent, AppComponentContext by context {
private val model: ForgetWalletModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
ForgetWalletContent(
state = state,
modifier = modifier,
)
}
@AssistedFactory
interface Factory : ForgetWalletComponent.Factory {
override fun create(
context: AppComponentContext,
params: ForgetWalletComponent.Params,
): DefaultForgetWalletComponent
}
}

View file

@ -0,0 +1,114 @@
package com.tangem.features.hotwallet.forgetwallet
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
import com.tangem.features.hotwallet.ForgetWalletComponent
import com.tangem.features.hotwallet.forgetwallet.entity.ForgetWalletUM
import com.tangem.features.hotwallet.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ModelScoped
internal class ForgetWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val deleteWalletUseCase: DeleteWalletUseCase,
private val uiMessageSender: UiMessageSender,
) : Model() {
private val params = paramsContainer.require<ForgetWalletComponent.Params>()
internal val uiState: StateFlow<ForgetWalletUM>
field = MutableStateFlow(
ForgetWalletUM(
onBackClick = { router.pop() },
firstCheckboxChecked = false,
secondCheckboxChecked = false,
onFirstCheckboxClick = ::onFirstCheckboxClick,
onSecondCheckboxClick = ::onSecondCheckboxClick,
onForgetWalletClick = ::onForgetWalletClick,
isForgetButtonEnabled = false,
),
)
private fun onFirstCheckboxClick() {
uiState.update {
val newValue = !it.firstCheckboxChecked
it.copy(
firstCheckboxChecked = newValue,
isForgetButtonEnabled = newValue && it.secondCheckboxChecked,
)
}
}
private fun onSecondCheckboxClick() {
uiState.update {
val newValue = !it.secondCheckboxChecked
it.copy(
secondCheckboxChecked = newValue,
isForgetButtonEnabled = it.firstCheckboxChecked && newValue,
)
}
}
private fun onForgetWalletClick() {
// TODO actualize strings [REDACTED_TASK_KEY]
uiMessageSender.send(
DialogMessage(
title = stringReference("Attention"),
message = stringReference("Are you sure you want to do this?"),
firstActionBuilder = {
EventMessageAction(
title = stringReference("Forget"),
isWarning = true,
onClick = ::forgetWallet,
)
},
secondActionBuilder = {
EventMessageAction(
title = stringReference("Cancel"),
onClick = {},
)
},
),
)
}
private fun forgetWallet() {
modelScope.launch {
val hasUserWallets = deleteWalletUseCase(params.userWalletId)
.getOrElse {
Timber.e("Unable to delete wallet: $it")
uiMessageSender.send(
message = SnackbarMessage(resourceReference(R.string.common_unknown_error)),
)
return@launch
}
if (hasUserWallets) {
router.pop()
} else {
router.replaceAll(AppRoute.Home())
}
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.features.hotwallet.forgetwallet.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.ForgetWalletComponent
import com.tangem.features.hotwallet.forgetwallet.DefaultForgetWalletComponent
import com.tangem.features.hotwallet.forgetwallet.ForgetWalletModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface ForgetWalletModule {
@Binds
fun bindForgetWalletComponentFactory(impl: DefaultForgetWalletComponent.Factory): ForgetWalletComponent.Factory
@Binds
@IntoMap
@ClassKey(ForgetWalletModel::class)
fun bindForgetWalletModel(model: ForgetWalletModel): Model
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.hotwallet.forgetwallet.entity
internal data class ForgetWalletUM(
val onBackClick: () -> Unit,
val firstCheckboxChecked: Boolean,
val secondCheckboxChecked: Boolean,
val onFirstCheckboxClick: () -> Unit,
val onSecondCheckboxClick: () -> Unit,
val onForgetWalletClick: () -> Unit,
val isForgetButtonEnabled: Boolean,
)

View file

@ -0,0 +1,155 @@
package com.tangem.features.hotwallet.forgetwallet.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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
import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.forgetwallet.entity.ForgetWalletUM
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun ForgetWalletContent(state: ForgetWalletUM, modifier: Modifier = Modifier) {
// TODO actualize strings [REDACTED_TASK_KEY]
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize()
.systemBarsPadding(),
) {
TangemTopAppBar(
modifier = Modifier.statusBarsPadding(),
startButton = TopAppBarButtonUM.Back(state.onBackClick),
title = TextReference.EMPTY,
)
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Spacer(modifier = Modifier.height(16.dp))
Icon(
painter = painterResource(R.drawable.ic_attention_72),
contentDescription = null,
tint = TangemTheme.colors.icon.warning,
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Attention",
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "This wallet will be permanently removed from your device",
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(16.dp))
}
Spacer(modifier = Modifier.height(48.dp))
CheckboxItem(
modifier = Modifier.padding(horizontal = 16.dp),
checked = state.firstCheckboxChecked,
onCheckedChange = state.onFirstCheckboxClick,
text = "I understand that removing my wallet does not delete it—but simply removes it from my device.",
)
Spacer(modifier = Modifier.height(16.dp))
CheckboxItem(
modifier = Modifier.padding(horizontal = 16.dp),
checked = state.secondCheckboxChecked,
onCheckedChange = state.onSecondCheckboxClick,
text = "I understand that if I haven't backed up my wallet before removing it, I may lose access to it.",
)
Spacer(modifier = Modifier.height(32.dp))
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = "Forget wallet",
onClick = state.onForgetWalletClick,
enabled = state.isForgetButtonEnabled,
)
}
}
@Composable
private fun CheckboxItem(checked: Boolean, onCheckedChange: () -> Unit, text: String, modifier: Modifier = Modifier) {
// TODO actualize strings [REDACTED_TASK_KEY]
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Top,
) {
IconToggleButton(
checked = checked,
onCheckedChange = { onCheckedChange() },
) {
AnimatedContent(
targetState = checked,
label = "Update checked state",
) { isChecked ->
Icon(
painter = painterResource(
if (isChecked) {
R.drawable.ic_accepted_20
} else {
R.drawable.ic_unticked_20
},
),
contentDescription = null,
tint = if (isChecked) {
TangemTheme.colors.control.checked
} else {
TangemTheme.colors.icon.secondary
},
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = text,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewForgetWalletContent() {
TangemThemePreview {
ForgetWalletContent(
state = ForgetWalletUM(
onBackClick = {},
firstCheckboxChecked = true,
secondCheckboxChecked = false,
onFirstCheckboxClick = {},
onSecondCheckboxClick = {},
onForgetWalletClick = {},
isForgetButtonEnabled = false,
),
)
}
}

View file

@ -18,6 +18,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.ui.components.bottomsheets.message.*
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
@ -205,24 +206,7 @@ internal class WalletSettingsModel @Inject constructor(
isNFTEnabled = isNFTEnabled,
onCheckedNFTChange = ::onCheckedNFTChange,
forgetWallet = {
val message = DialogMessage(
message = resourceReference(
id = when (userWallet) {
is UserWallet.Cold -> R.string.user_wallet_list_delete_prompt
is UserWallet.Hot -> R.string.user_wallet_list_delete_hw_prompt
},
),
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_delete),
isWarning = true,
onClick = ::forgetWallet,
)
},
secondActionBuilder = { cancelAction() },
)
messageSender.send(message)
onForgetWalletClick(userWallet)
},
onLinkMoreCardsClick = {
when (userWallet) {
@ -403,4 +387,80 @@ internal class WalletSettingsModel @Inject constructor(
}
}
}
// TODO actualize strings [REDACTED_TASK_KEY] and remove MaximumLineLength
@Suppress("LongMethod", "MaximumLineLength")
private fun onForgetWalletClick(userWallet: UserWallet) {
val message = when (userWallet) {
is UserWallet.Cold -> {
DialogMessage(
message = resourceReference(
id = R.string.user_wallet_list_delete_prompt,
),
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_delete),
isWarning = true,
onClick = ::forgetWallet,
)
},
secondActionBuilder = { cancelAction() },
)
}
is UserWallet.Hot -> {
bottomSheetMessage {
infoBlock {
icon(R.drawable.ic_alert_circle_24) {
type = MessageBottomSheetUMV2.Icon.Type.Warning
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
}
title = stringReference("Forget this wallet?")
body = if (userWallet.backedUp) {
stringReference("A backup for this wallet exists. Review it before forgetting to make sure you can recover later.")
} else {
stringReference("If you forget this wallet without a backup, youll permanently lose access to your funds.")
}
}
if (userWallet.backedUp) {
secondaryButton {
text = stringReference("Forget wallet")
onClick {
router.push(AppRoute.ForgetWallet(userWallet.walletId))
closeBs()
}
}
} else {
secondaryButton {
text = stringReference("Forget Anyway")
onClick {
router.push(AppRoute.ForgetWallet(userWallet.walletId))
closeBs()
}
}
}
if (userWallet.backedUp) {
primaryButton {
text = stringReference("View backup")
onClick {
unlockWalletIfNeedAndProceed {
router.push(AppRoute.ViewPhrase(userWallet.walletId))
}
closeBs()
}
}
} else {
primaryButton {
text = stringReference("Go to backup")
onClick {
router.push(AppRoute.CreateWalletBackup(userWallet.walletId))
closeBs()
}
}
}
}
}
}
messageSender.send(message)
}
}