Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-21 14:35:01 +05:00
parent f009a78876
commit e07246d25d
33 changed files with 736 additions and 87 deletions

View file

@ -63,6 +63,7 @@ internal class ChildFactory @Inject constructor(
private val detailsComponentFactory: DetailsComponent.Factory,
private val walletSettingsComponentFactory: WalletSettingsComponent.Factory,
private val walletBackupComponentFactory: WalletBackupComponent.Factory,
private val walletHardwareBackupComponentFactory: WalletHardwareBackupComponent.Factory,
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
@ -189,6 +190,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = walletBackupComponentFactory,
)
}
is AppRoute.WalletHardwareBackup -> {
createComponentChild(
context = context,
params = WalletHardwareBackupComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = walletHardwareBackupComponentFactory,
)
}
is AppRoute.MarketsTokenDetails -> {
createComponentChild(
context = context,
@ -542,6 +552,7 @@ internal class ChildFactory @Inject constructor(
context = context,
params = CreateWalletBackupComponent.Params(
userWalletId = route.userWalletId,
isUpgradeFlow = route.isUpgradeFlow,
),
componentFactory = createWalletBackupComponentFactory,
)

View file

@ -220,6 +220,11 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_backup/${userWalletId.stringValue}")
@Serializable
data class WalletHardwareBackup(
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_hardware_backup/${userWalletId.stringValue}")
@Serializable
data object Markets : AppRoute(path = "/markets")
@ -347,6 +352,7 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class CreateWalletBackup(
val userWalletId: UserWalletId,
val isUpgradeFlow: Boolean,
) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}")
@Serializable

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -8,6 +8,7 @@ interface CreateWalletBackupComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val isUpgradeFlow: Boolean,
)
interface Factory : ComponentFactory<Params, CreateWalletBackupComponent>

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 WalletHardwareBackupComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
)
interface Factory : ComponentFactory<Params, WalletHardwareBackupComponent>
}

View file

@ -120,6 +120,8 @@ internal class AddExistingWalletModel @Inject constructor(
override fun onContinueClick(userWalletId: UserWalletId) {
stackNavigation.replaceAll(AddExistingWalletRoute.SetAccessCode(userWalletId))
}
override fun onUpgradeClick(userWalletId: UserWalletId) = Unit
}
inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks {

View file

@ -34,6 +34,7 @@ internal class AddExistingWalletChildFactory @Inject constructor(
params = ManualBackupCompletedComponent.Params(
userWalletId = route.userWalletId,
callbacks = model.manualBackupCompletedComponentModelCallbacks,
isUpgradeFlow = false,
),
)
is AddExistingWalletRoute.SetAccessCode -> accessCodeComponentFactory.create(

View file

@ -3,11 +3,13 @@ package com.tangem.features.hotwallet.createwalletbackup
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.push
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.utils.AnalyticsContextProxy
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.navigation.popTo
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.hotwallet.CreateWalletBackupComponent
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute
@ -65,7 +67,7 @@ internal class CreateWalletBackupModel @Inject constructor(
}
fun onManualBackupChecked() {
stackNavigation.push(CreateWalletBackupRoute.BackupCompleted)
stackNavigation.push(CreateWalletBackupRoute.BackupCompleted(isUpgradeFlow = params.isUpgradeFlow))
}
fun onManualBackupCompleted() {
@ -94,5 +96,14 @@ internal class CreateWalletBackupModel @Inject constructor(
override fun onContinueClick(userWalletId: UserWalletId) {
onManualBackupCompleted()
}
override fun onUpgradeClick(userWalletId: UserWalletId) {
router.popTo<AppRoute.WalletSettings>()
router.push(
AppRoute.UpgradeWallet(
userWalletId = userWalletId,
),
)
}
}
}

View file

@ -16,31 +16,32 @@ internal class CreateWalletBackupChildFactory @Inject constructor() {
childContext: AppComponentContext,
model: CreateWalletBackupModel,
): ComposableContentComponent = when (route) {
CreateWalletBackupRoute.RecoveryPhraseStart -> ManualBackupStartComponent(
is CreateWalletBackupRoute.RecoveryPhraseStart -> ManualBackupStartComponent(
context = childContext,
params = ManualBackupStartComponent.Params(
callbacks = model.manualBackupStartModelCallbacks,
),
)
CreateWalletBackupRoute.RecoveryPhrase -> ManualBackupPhraseComponent(
is CreateWalletBackupRoute.RecoveryPhrase -> ManualBackupPhraseComponent(
context = childContext,
params = ManualBackupPhraseComponent.Params(
userWalletId = model.params.userWalletId,
callbacks = model.manualBackupPhraseModelCallbacks,
),
)
CreateWalletBackupRoute.ConfirmBackup -> ManualBackupCheckComponent(
is CreateWalletBackupRoute.ConfirmBackup -> ManualBackupCheckComponent(
context = childContext,
params = ManualBackupCheckComponent.Params(
userWalletId = model.params.userWalletId,
callbacks = model.manualBackupCheckModelCallbacks,
),
)
CreateWalletBackupRoute.BackupCompleted -> ManualBackupCompletedComponent(
is CreateWalletBackupRoute.BackupCompleted -> ManualBackupCompletedComponent(
context = childContext,
params = ManualBackupCompletedComponent.Params(
userWalletId = model.params.userWalletId,
callbacks = model.manualBackupCompletedModelCallbacks,
isUpgradeFlow = route.isUpgradeFlow,
),
)
}

View file

@ -15,5 +15,7 @@ internal sealed interface CreateWalletBackupRoute {
data object ConfirmBackup : CreateWalletBackupRoute
@Serializable
data object BackupCompleted : CreateWalletBackupRoute
data class BackupCompleted(
val isUpgradeFlow: Boolean,
) : CreateWalletBackupRoute
}

View file

@ -29,10 +29,12 @@ internal class ManualBackupCompletedComponent @AssistedInject constructor(
interface ModelCallbacks {
fun onContinueClick(userWalletId: UserWalletId)
fun onUpgradeClick(userWalletId: UserWalletId)
}
data class Params(
val userWalletId: UserWalletId,
val callbacks: ModelCallbacks,
val isUpgradeFlow: Boolean,
)
}

View file

@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class ManualBackupCompletedModel @Inject constructor(
paramsContainer: ParamsContainer,
@ -20,7 +21,14 @@ internal class ManualBackupCompletedModel @Inject constructor(
internal val uiState: StateFlow<ManualBackupCompletedUM>
field = MutableStateFlow(
ManualBackupCompletedUM(
onContinueClick = { params.callbacks.onContinueClick(params.userWalletId) },
onContinueClick = {
if (params.isUpgradeFlow) {
params.callbacks.onUpgradeClick(params.userWalletId)
} else {
params.callbacks.onContinueClick(params.userWalletId)
}
},
isLoading = false,
),
)
}

View file

@ -2,4 +2,5 @@ package com.tangem.features.hotwallet.manualbackup.completed.entity
internal data class ManualBackupCompletedUM(
val onContinueClick: () -> Unit,
val isLoading: Boolean,
)

View file

@ -65,8 +65,6 @@ internal fun ManualBackupCompletedContent(state: ManualBackupCompletedUM, modifi
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(R.string.common_continue),
showProgress = false,
enabled = true,
onClick = state.onContinueClick,
)
}
@ -75,11 +73,26 @@ internal fun ManualBackupCompletedContent(state: ManualBackupCompletedUM, modifi
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewManualBackupCompletedContent() {
private fun PreviewManualBackupCompletedContentRegular() {
TangemThemePreview {
ManualBackupCompletedContent(
state = ManualBackupCompletedUM(
onContinueClick = {},
isLoading = false,
onContinueClick = { },
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewManualBackupCompletedContentUpgrade() {
TangemThemePreview {
ManualBackupCompletedContent(
state = ManualBackupCompletedUM(
isLoading = false,
onContinueClick = { },
),
)
}

View file

@ -53,14 +53,14 @@ internal class UpgradeWalletModel @Inject constructor(
) : Model() {
private val params = paramsContainer.require<UpgradeWalletComponent.Params>()
private val _uiState = MutableStateFlow(
UpgradeWalletUM(
onBackClick = { router.pop() },
onBuyTangemWalletClick = ::onBuyTangemWalletClick,
onScanDeviceClick = ::onScanDeviceClick,
),
)
internal val uiState: StateFlow<UpgradeWalletUM> = _uiState
internal val uiState: StateFlow<UpgradeWalletUM>
field = MutableStateFlow(
UpgradeWalletUM(
onBackClick = { router.pop() },
onBuyTangemWalletClick = ::onBuyTangemWalletClick,
onContinueClick = ::onContinueClick,
),
)
override fun onDestroy() {
clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
@ -73,7 +73,7 @@ internal class UpgradeWalletModel @Inject constructor(
}
}
private fun onScanDeviceClick() {
private fun onContinueClick() {
scanCard()
}
@ -100,7 +100,7 @@ internal class UpgradeWalletModel @Inject constructor(
}
private fun setLoading(isLoading: Boolean) {
_uiState.update { it.copy(isLoading = isLoading) }
uiState.update { it.copy(isLoading = isLoading) }
}
private fun showCardVerificationFailedDialog(error: TangemError) {

View file

@ -3,6 +3,6 @@ package com.tangem.features.hotwallet.upgradewallet.entity
internal data class UpgradeWalletUM(
val onBackClick: () -> Unit,
val onBuyTangemWalletClick: () -> Unit,
val onScanDeviceClick: () -> Unit,
val onContinueClick: () -> Unit,
val isLoading: Boolean = false,
)

View file

@ -3,7 +3,9 @@ package com.tangem.features.hotwallet.upgradewallet.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -103,11 +105,11 @@ internal fun UpgradeWalletContent(state: UpgradeWalletUM, modifier: Modifier = M
onClick = state.onBuyTangemWalletClick,
)
PrimaryButtonIconEnd(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(R.string.hw_upgrade_scan_device),
onClick = state.onScanDeviceClick,
modifier = Modifier.fillMaxWidth(),
iconResId = R.drawable.ic_tangem_24,
text = stringResourceSafe(R.string.hw_upgrade_start_action),
onClick = state.onContinueClick,
showProgress = state.isLoading,
)
}
}
@ -116,13 +118,28 @@ internal fun UpgradeWalletContent(state: UpgradeWalletUM, modifier: Modifier = M
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewUpgradeWalletContent() {
private fun PreviewUpgradeWalletContentBackup() {
TangemThemePreview {
UpgradeWalletContent(
state = UpgradeWalletUM(
onBackClick = {},
onBuyTangemWalletClick = {},
onScanDeviceClick = {},
onContinueClick = {},
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewUpgradeWalletContentScan() {
TangemThemePreview {
UpgradeWalletContent(
state = UpgradeWalletUM(
onBackClick = {},
onBuyTangemWalletClick = {},
onContinueClick = {},
),
)
}

View file

@ -147,6 +147,8 @@ internal class WalletActivationModel @Inject constructor(
override fun onContinueClick(userWalletId: UserWalletId) {
stackNavigation.push(WalletActivationRoute.SetAccessCode)
}
override fun onUpgradeClick(userWalletId: UserWalletId) = Unit
}
inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks {

View file

@ -50,6 +50,7 @@ internal class WalletActivationChildFactory @Inject constructor(
params = ManualBackupCompletedComponent.Params(
userWalletId = model.params.userWalletId,
callbacks = model.manualBackupCompletedModelCallbacks,
isUpgradeFlow = false,
),
)
is WalletActivationRoute.SetAccessCode -> accessCodeComponentFactory.create(

View file

@ -7,6 +7,7 @@ internal data class WalletBackupUM(
val recoveryPhraseOption: LabelUM?,
val googleDriveOption: LabelUM?,
val googleDriveStatus: BackupStatus,
val onBuyClick: () -> Unit,
val onRecoveryPhraseClick: () -> Unit,
val onGoogleDriveClick: () -> Unit,
val onHardwareWalletClick: () -> Unit,

View file

@ -1,16 +1,18 @@
package com.tangem.features.hotwallet.walletbackup.model
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.navigation.url.UrlOpener
import com.tangem.core.ui.R
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.common.routing.AppRoute
import com.tangem.domain.wallets.usecase.UnlockHotWalletContextualUseCase
import com.tangem.features.hotwallet.WalletBackupComponent
import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus
@ -21,12 +23,15 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class WalletBackupModel @Inject constructor(
paramsContainer: ParamsContainer,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase,
override val dispatchers: CoroutineDispatcherProvider,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener,
private val router: Router,
) : Model() {
@ -45,6 +50,7 @@ internal class WalletBackupModel @Inject constructor(
style = LabelStyle.REGULAR,
),
googleDriveStatus = BackupStatus.ComingSoon,
onBuyClick = ::onBuyClick,
onRecoveryPhraseClick = ::onRecoveryPhraseClick,
onGoogleDriveClick = { },
onHardwareWalletClick = ::onHardwareWalletClick,
@ -95,6 +101,12 @@ internal class WalletBackupModel @Inject constructor(
backedUp = userWallet.backedUp,
)
private fun onBuyClick() {
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
}
}
private fun onRecoveryPhraseClick() {
if (uiState.value.backedUp) {
getUserWalletUseCase.invoke(params.userWalletId)
@ -113,7 +125,12 @@ internal class WalletBackupModel @Inject constructor(
},
)
} else {
router.push(AppRoute.CreateWalletBackup(params.userWalletId))
router.push(
AppRoute.CreateWalletBackup(
userWalletId = params.userWalletId,
isUpgradeFlow = false,
),
)
}
}
@ -132,6 +149,6 @@ internal class WalletBackupModel @Inject constructor(
}
private fun onHardwareWalletClick() {
router.push(AppRoute.UpgradeWallet(params.userWalletId))
router.push(AppRoute.WalletHardwareBackup(params.userWalletId))
}
}

View file

@ -1,35 +1,43 @@
package com.tangem.features.hotwallet.walletbackup.ui
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
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.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus
import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM
import com.tangem.features.hotwallet.common.ui.OptionBlock
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.components.rows.NetworkTitle
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.ForceDarkTheme
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.common.ui.OptionBlock
import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus
import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod")
@Composable
internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Modifier) {
Column(
@ -46,11 +54,40 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
.verticalScroll(rememberScrollState())
.padding(
start = 16.dp,
top = 12.dp,
end = 16.dp,
),
) {
Banner(state)
OptionBlock(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
title = stringResourceSafe(R.string.hw_backup_hardware_title),
description = stringResourceSafe(R.string.hw_backup_hardware_description),
badge = null,
onClick = state.onHardwareWalletClick,
enabled = true,
backgroundColor = TangemTheme.colors.background.primary,
)
NetworkTitle(
modifier = Modifier
.padding(top = 8.dp),
title = {
Text(
modifier = Modifier,
text = stringResourceSafe(R.string.onboarding_create_wallet_options_button_options),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
)
OptionBlock(
modifier = Modifier,
title = stringResourceSafe(R.string.hw_backup_seed_title),
description = stringResourceSafe(R.string.hw_backup_seed_description),
badge = {
@ -60,7 +97,6 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod
enabled = true,
backgroundColor = TangemTheme.colors.background.primary,
)
OptionBlock(
modifier = Modifier
.padding(top = 8.dp),
@ -73,30 +109,125 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod
enabled = state.googleDriveStatus != BackupStatus.ComingSoon,
backgroundColor = TangemTheme.colors.background.primary,
)
NetworkTitle(
title = {
Text(
modifier = Modifier,
text = stringResourceSafe(R.string.express_provider_recommended),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
)
OptionBlock(
modifier = Modifier.fillMaxWidth(),
title = stringResourceSafe(R.string.hw_backup_hardware_title),
description = stringResourceSafe(R.string.hw_backup_hardware_description),
badge = null,
onClick = state.onHardwareWalletClick,
enabled = true,
backgroundColor = TangemTheme.colors.background.primary,
)
Spacer(modifier = Modifier.size(16.dp))
}
}
}
@Suppress("LongMethod")
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun Banner(state: WalletBackupUM, modifier: Modifier = Modifier) {
ForceDarkTheme {
Column(
modifier = modifier
.background(
color = TangemTheme.colors.background.primary,
shape = RoundedCornerShape(16.dp),
),
) {
Column(
modifier = Modifier
.padding(
start = 12.dp,
top = 20.dp,
end = 12.dp,
),
) {
Text(
modifier = Modifier
.fillMaxWidth(),
text = "Tangem Wallet",
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
top = 4.dp,
),
text = "Keeps your crypto safe and offline. Slim as a credit card, safer than a bank vault.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
FlowRow(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 24.dp,
top = 16.dp,
end = 24.dp,
),
horizontalArrangement = Arrangement.Center,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
FeatureItem(
iconResId = R.drawable.ic_shield_check_16,
text = resourceReference(R.string.welcome_create_wallet_feature_class),
)
FeatureItem(
iconResId = R.drawable.ic_flash_16,
text = resourceReference(R.string.welcome_create_wallet_feature_delivery),
)
FeatureItem(
iconResId = R.drawable.ic_sparkles_16,
text = resourceReference(R.string.welcome_create_wallet_feature_use),
)
}
Box(
modifier = Modifier
.padding(
start = 8.dp,
top = 12.dp,
end = 8.dp,
bottom = 20.dp,
),
) {
Image(
painter = painterResource(id = R.drawable.img_tangem_cards_vertical),
contentDescription = null,
)
SecondaryButton(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
text = stringResourceSafe(R.string.details_buy_wallet),
onClick = state.onBuyClick,
)
}
}
}
}
}
@Composable
private fun FeatureItem(@DrawableRes iconResId: Int, text: TextReference) {
Row(
modifier = Modifier
.wrapContentWidth()
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Icon(
modifier = Modifier.size(16.dp),
painter = painterResource(iconResId),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
Text(
text = text.resolveReference(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.secondary,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@ -119,6 +250,7 @@ private class WalletBackupUMProvider : CollectionPreviewParameterProvider<Wallet
),
googleDriveStatus = BackupStatus.ComingSoon,
onBackClick = {},
onBuyClick = {},
onRecoveryPhraseClick = {},
onGoogleDriveClick = {},
onHardwareWalletClick = {},
@ -135,6 +267,7 @@ private class WalletBackupUMProvider : CollectionPreviewParameterProvider<Wallet
),
googleDriveStatus = BackupStatus.NoBackup,
onBackClick = {},
onBuyClick = {},
onRecoveryPhraseClick = {},
onGoogleDriveClick = {},
onHardwareWalletClick = {},
@ -151,6 +284,7 @@ private class WalletBackupUMProvider : CollectionPreviewParameterProvider<Wallet
),
googleDriveStatus = BackupStatus.Done,
onBackClick = {},
onBuyClick = {},
onRecoveryPhraseClick = {},
onGoogleDriveClick = {},
onHardwareWalletClick = {},

View file

@ -0,0 +1,39 @@
package com.tangem.features.hotwallet.wallethardwarebackup.component
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.WalletHardwareBackupComponent
import com.tangem.features.hotwallet.wallethardwarebackup.model.WalletHardwareBackupModel
import com.tangem.features.hotwallet.wallethardwarebackup.ui.WalletHardwareBackupContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultWalletHardwareBackupComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: WalletHardwareBackupComponent.Params,
) : WalletHardwareBackupComponent, AppComponentContext by context {
private val model: WalletHardwareBackupModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
WalletHardwareBackupContent(
state = state,
modifier = modifier,
)
}
@AssistedFactory
interface Factory : WalletHardwareBackupComponent.Factory {
override fun create(
context: AppComponentContext,
params: WalletHardwareBackupComponent.Params,
): DefaultWalletHardwareBackupComponent
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.features.hotwallet.wallethardwarebackup.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.WalletHardwareBackupComponent
import com.tangem.features.hotwallet.wallethardwarebackup.component.DefaultWalletHardwareBackupComponent
import com.tangem.features.hotwallet.wallethardwarebackup.model.WalletHardwareBackupModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object WalletHardwareBackupModule
@Module
@InstallIn(SingletonComponent::class)
internal interface WalletHardwareBackupModuleBinds {
@Binds
@Singleton
fun bindWalletHardwareBackupComponentFactory(
impl: DefaultWalletHardwareBackupComponent.Factory,
): WalletHardwareBackupComponent.Factory
@Binds
@IntoMap
@ClassKey(WalletHardwareBackupModel::class)
fun bindWalletHardwareBackupModel(model: WalletHardwareBackupModel): Model
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.hotwallet.wallethardwarebackup.entity
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
internal data class WalletHardwareBackupUM(
val showPurchaseBlock: Boolean = false,
val blocks: ImmutableList<Block>,
val onBackClick: () -> Unit,
val onBuyClick: () -> Unit,
) {
data class Block(
val title: TextReference,
val titleLabel: LabelUM?,
val description: TextReference,
val onClick: () -> Unit,
)
}

View file

@ -0,0 +1,143 @@
package com.tangem.features.hotwallet.wallethardwarebackup.model
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.navigation.url.UrlOpener
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
import com.tangem.core.ui.components.bottomsheets.message.icon
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
import com.tangem.core.ui.components.bottomsheets.message.onClick
import com.tangem.core.ui.components.bottomsheets.message.primaryButton
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.hotwallet.WalletHardwareBackupComponent
import com.tangem.features.hotwallet.impl.R
import com.tangem.features.hotwallet.wallethardwarebackup.entity.WalletHardwareBackupUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class WalletHardwareBackupModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val messageSender: UiMessageSender,
) : Model() {
private val params = paramsContainer.require<WalletHardwareBackupComponent.Params>()
// TODO actualize strings [REDACTED_TASK_KEY]
private val makeBackupAtFirstAlertBS
get() = bottomSheetMessage {
infoBlock {
icon(R.drawable.ic_passcode_lock_32) {
type = MessageBottomSheetUMV2.Icon.Type.Accent
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
}
title = stringReference("Finish Backup First")
body = stringReference("To upgrade your wallet to hardware, back it up first.")
}
primaryButton {
text = resourceReference(R.string.hw_backup_need_action)
onClick {
router.push(
AppRoute.CreateWalletBackup(
userWalletId = params.userWalletId,
isUpgradeFlow = true,
),
)
closeBs()
}
}
}
// TODO actualize strings [REDACTED_TASK_KEY]
internal val uiState: StateFlow<WalletHardwareBackupUM>
field = MutableStateFlow(
WalletHardwareBackupUM(
onBackClick = { router.pop() },
blocks = persistentListOf(
WalletHardwareBackupUM.Block(
title = stringReference("Create new wallet"),
titleLabel = LabelUM(
text = resourceReference(R.string.common_recommended),
style = LabelStyle.ACCENT,
),
description = stringReference(
"Create a new secure wallet and transfer your funds for extra protection.",
),
onClick = ::onCreateNewWalletClick,
),
WalletHardwareBackupUM.Block(
title = stringReference("Upgrade current wallet"),
titleLabel = null,
description = stringReference("Move your current wallet into Tangem Wallet."),
onClick = ::onUpgradeCurrentWalletClick,
),
),
onBuyClick = ::onBuyClick,
),
)
init {
showPurchaseBlockWithDelay()
}
private fun showPurchaseBlockWithDelay() {
modelScope.launch {
delay(SHOW_PURCHASE_BLOCK_DELAY)
uiState.update { it.copy(showPurchaseBlock = true) }
}
}
private fun onCreateNewWalletClick() {
router.push(AppRoute.CreateHardwareWallet)
}
private fun onUpgradeCurrentWalletClick() {
val userWallet = getUserWalletUseCase.invoke(params.userWalletId)
.getOrElse { error("Cannot find user wallet with id: ${params.userWalletId.stringValue}") }
if (userWallet is UserWallet.Hot) {
if (!userWallet.backedUp) {
messageSender.send(makeBackupAtFirstAlertBS)
} else {
router.push(
AppRoute.UpgradeWallet(
userWalletId = params.userWalletId,
),
)
}
}
}
private fun onBuyClick() {
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
}
}
companion object {
private const val SHOW_PURCHASE_BLOCK_DELAY = 3000L
}
}

View file

@ -0,0 +1,162 @@
package com.tangem.features.hotwallet.wallethardwarebackup.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
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.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.common.ui.OptionBlock
import com.tangem.features.hotwallet.impl.R
import com.tangem.features.hotwallet.wallethardwarebackup.entity.WalletHardwareBackupUM
import kotlinx.collections.immutable.persistentListOf
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun WalletHardwareBackupContent(state: WalletHardwareBackupUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.secondary)
.fillMaxSize()
.systemBarsPadding(),
) {
TopAppBar(
modifier = Modifier
.statusBarsPadding(),
colors = TopAppBarDefaults.topAppBarColors(
containerColor = TangemTheme.colors.background.secondary,
),
navigationIcon = {
IconButton(onClick = state.onBackClick) {
Icon(
painter = painterResource(R.drawable.ic_back_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
}
},
title = {
Text(
text = stringResourceSafe(R.string.hw_backup_hardware_title),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
)
Column(
modifier = Modifier
.weight(1f)
.padding(
start = 16.dp,
top = 4.dp,
end = 16.dp,
),
) {
state.blocks.forEach { block ->
OptionBlock(
modifier = Modifier
.padding(top = 8.dp),
title = block.title.resolveReference(),
description = block.description.resolveReference(),
badge = block.titleLabel?.let {
{ Label(it) }
},
enabled = true,
backgroundColor = TangemTheme.colors.background.primary,
onClick = block.onClick,
)
}
}
AnimatedVisibility(state.showPurchaseBlock) {
PurchaseBlock(
onBuyClick = state.onBuyClick,
)
}
}
}
@Composable
private fun PurchaseBlock(onBuyClick: () -> Unit, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
.background(
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(
horizontal = 20.dp,
vertical = 16.dp,
),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.weight(1f)
.padding(end = 16.dp),
text = stringResourceSafe(R.string.wallet_add_hardware_purchase),
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
SecondaryButton(
text = stringResourceSafe(R.string.wallet_import_buy_title),
onClick = onBuyClick,
size = TangemButtonSize.RoundedAction,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewWalletHardwareBackupContent() {
TangemThemePreview {
WalletHardwareBackupContent(
state = WalletHardwareBackupUM(
onBackClick = { },
blocks = persistentListOf(
WalletHardwareBackupUM.Block(
title = stringReference("Create new wallet"),
titleLabel = LabelUM(
text = resourceReference(R.string.common_recommended),
style = LabelStyle.ACCENT,
),
description = stringReference(
"Create a new secure wallet and transfer your funds for extra protection.",
),
onClick = { },
),
WalletHardwareBackupUM.Block(
title = stringReference("Upgrade current wallet"),
titleLabel = null,
description = stringReference("Move your current wallet into Tangem Wallet."),
onClick = { },
),
),
showPurchaseBlock = true,
onBuyClick = { },
),
)
}
}

View file

@ -38,11 +38,7 @@ import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.walletsettings.analytics.Settings
import com.tangem.feature.walletsettings.analytics.WalletSettingsAnalyticEvents
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.feature.walletsettings.entity.DialogConfig
import com.tangem.feature.walletsettings.entity.NetworksAvailableForNotificationBSConfig
import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM
import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM
import com.tangem.feature.walletsettings.entity.WalletSettingsUM
import com.tangem.feature.walletsettings.entity.*
import com.tangem.feature.walletsettings.impl.R
import com.tangem.feature.walletsettings.utils.AccountItemsDelegate
import com.tangem.feature.walletsettings.utils.ItemsBuilder
@ -112,10 +108,15 @@ internal class WalletSettingsModel @Inject constructor(
title = resourceReference(R.string.hw_backup_need_title)
body = resourceReference(R.string.hw_backup_need_description)
}
secondaryButton {
primaryButton {
text = resourceReference(R.string.hw_backup_need_action)
onClick {
router.push(AppRoute.CreateWalletBackup(params.userWalletId))
router.push(
AppRoute.CreateWalletBackup(
userWalletId = params.userWalletId,
isUpgradeFlow = false,
),
)
closeBs()
}
}
@ -354,7 +355,11 @@ internal class WalletSettingsModel @Inject constructor(
private fun onUpgradeWalletClick() {
unlockWalletIfNeedAndProceed {
router.push(AppRoute.UpgradeWallet(params.userWalletId))
router.push(
AppRoute.UpgradeWallet(
userWalletId = params.userWalletId,
),
)
}
}
@ -452,7 +457,12 @@ internal class WalletSettingsModel @Inject constructor(
primaryButton {
text = stringReference("Go to backup")
onClick {
router.push(AppRoute.CreateWalletBackup(userWallet.walletId))
router.push(
AppRoute.CreateWalletBackup(
userWalletId = userWallet.walletId,
isUpgradeFlow = false,
),
)
closeBs()
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.feature.wallet.utils
import arrow.core.Either
import com.tangem.common.ui.userwallet.converter.ArtworkUMConverter
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.artwork.ArtworkUM
@ -45,16 +44,15 @@ class DefaultUserWalletImageFetcher @Inject constructor(
.map { allWallets -> allWallets.filter { (walletId, _) -> wallets.any { it.walletId == walletId } } }
.distinctUntilChanged()
override fun walletImage(walletId: UserWalletId, size: ArtworkSize): Flow<UserWalletItemUM.ImageState> = flow {
val imagesFlow = getUserWalletUseCase.invokeFlow(walletId)
// emit Loading and wait wallet
.onEach { if (it.isLeft()) emit(UserWalletItemUM.ImageState.Loading) }
.filterIsInstance<Either.Right<UserWallet>>()
.map { it.value }
override fun walletImage(walletId: UserWalletId, size: ArtworkSize): Flow<UserWalletItemUM.ImageState> =
getUserWalletUseCase.invokeFlow(walletId)
.transformLatest {
it.fold(
ifLeft = { emit(UserWalletItemUM.ImageState.Loading) },
ifRight = { wallet -> emitAll(walletImage(wallet, size)) },
)
}
.distinctUntilChanged()
.flatMapLatest { wallet -> walletImage(wallet, size) }
emitAll(imagesFlow)
}.distinctUntilChanged()
override fun walletImage(cardDTO: CardDTO, size: ArtworkSize): Flow<UserWalletItemUM.ImageState> =
internalGetCardImage(