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

@ -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)
}
}