Updated on 2026-08-14

This commit is contained in:
Tangem 2025-11-19 09:16:46 +03:00
commit 6437018fc8
1160 changed files with 35466 additions and 6540 deletions

View file

@ -0,0 +1,10 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:PortfolioFetcher.kt$PortfolioFetcher.Mode.All$val onlyMultiCurrency: Boolean</ID>
<ID>NonBooleanPropertyPrefixedWithIs:PortfolioSelectorComponent.kt$PortfolioSelectorController$/** * for some Feature specific filtering * combine and update with your Feature data and [PortfolioFetcher.data] */ val isEnabled: MutableStateFlow&lt;(UserWallet, AccountStatus) -&gt; Boolean&gt;</ID>
<ID>NonBooleanPropertyPrefixedWithIs:PortfolioSelectorComponent.kt$PortfolioSelectorController$val isAccountMode: Flow&lt;Boolean&gt;</ID>
<ID>UseSumOfInsteadOfFlatMapSize:PortfolioFetcher.kt$PortfolioFetcher.Data$flatten()</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -21,8 +21,18 @@ interface PortfolioFetcher {
data class Data(
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
val balances: Map<UserWallet, PortfolioBalance>,
)
val balances: Map<UserWalletId, PortfolioBalance>,
) {
val isSingleChoice: Boolean = balances.values
.map { it.accountsBalance.accountStatuses }
.flatten().size == 1
fun isSingleChoice(walletId: UserWalletId): Boolean = balances[walletId]
?.accountsBalance
?.accountStatuses
?.size == 1
}
data class PortfolioBalance(
val userWallet: UserWallet,

View file

@ -0,0 +1,29 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:AccountCreateEditModel.kt$AccountCreateEditModel$val duplicateAccountNames = (error as? AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet) ?.cause is AccountList.Error.DuplicateAccountNames</ID>
<ID>BooleanPropertyNaming:AccountCreateEditModel.kt$AccountCreateEditModel$val duplicateAccountNames = (error as? UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet) ?.cause is AccountList.Error.DuplicateAccountNames</ID>
<ID>BooleanPropertyNaming:AccountCreateEditModel.kt$AccountCreateEditModel$val showConfirmDialog = uiState.value.buttonState.isButtonEnabled</ID>
<ID>BooleanPropertyNaming:AccountCreateEditUM.kt$AccountCreateEditUM.Button$val showProgress: Boolean</ID>
<ID>MultilineLambdaItParameter:AccountCreateEditContent.kt${ /* * If the user had the default main account name and enters the same name during renaming, * we should use the default value instead of custom to avoid breaking the name validation process. */ val newName = if (wasDefault &amp;&amp; it == defaultAccountName) { AccountNameUM.DefaultMain } else { AccountNameUM.Custom(raw = it) } account.onNameChange(newName) }</ID>
<ID>MultilineLambdaItParameter:ArchivedAccountListContent.kt$PreviewStateProvider${ ArchivedAccountUM( accountId = it.toString(), accountName = accountName, accountIconUM = portfolioIcon(), tokensInfo = stringReference("10 tokens"), networksInfo = stringReference("2 networks"), onClick = {}, isLoading = it % 2 == 0, ) }</ID>
<ID>NamedArguments:PortfolioSelectorModel.kt$PortfolioSelectorModel$buildAccountsList(portfolioData, artworks, isEnabled, selectedAccount)</ID>
<ID>NamedArguments:PortfolioSelectorModel.kt$PortfolioSelectorModel$buildUiList(isAccountsMode, portfolioData, artworks, isEnabled, selectedAccount)</ID>
<ID>NamedArguments:PortfolioSelectorModel.kt$PortfolioSelectorModel$buildWalletList(portfolioData, artworks, isEnabled, selectedAccount)</ID>
<ID>NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$override val isAccountMode: Flow&lt;Boolean&gt; by lazy { isAccountsModeEnabledUseCase() }</ID>
<ID>NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$override val isEnabled: MutableStateFlow&lt;(UserWallet, AccountStatus) -&gt; Boolean&gt; = MutableStateFlow { _, _ -&gt; true }</ID>
<ID>NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
<ID>NonBooleanPropertyPrefixedWithIs:PortfolioSelectorModel.kt$PortfolioSelectorModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
<ID>NullableBooleanCheck:AccountDetailsModel.kt$AccountDetailsModel$getUserWalletUseCase(account.accountId.userWalletId) .getOrNull()?.isMultiCurrency ?: false</ID>
<ID>PropertyUsedBeforeDeclaration:AccountDetailsModel.kt$AccountDetailsModel$_uiState</ID>
<ID>PropertyUsedBeforeDeclaration:ArchivedAccountListModel.kt$ArchivedAccountListModel$_uiState</ID>
<ID>UnnecessaryLet:ArchivedAccountListModel.kt$ArchivedAccountListModel$let { _uiState.value = newState }</ID>
<ID>UnnecessaryLet:PortfolioSelectorContent.kt$PortfolioSelectorPreviewData$let(::add)</ID>
<ID>UnusedImports:PortfolioSelectorBS.kt$import androidx.compose.foundation.background</ID>
<ID>UseEmptyCounterpart:AccountCreateEditModel.kt$AccountCreateEditModel$mapOf()</ID>
<ID>UseEmptyCounterpart:ArchivedAccountListModel.kt$ArchivedAccountListModel$mapOf()</ID>
<ID>VarCouldBeVal:AccountDetailsContent.kt$PreviewStateProvider$var portfolioIcon = AccountIconPreviewData.randomAccountIcon()</ID>
<ID>VarCouldBeVal:ArchivedAccountListModel.kt$ArchivedAccountListModel$private var getArchivedAccountsJob = JobHolder()</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -1,6 +1,5 @@
package com.tangem.features.account.archived
import com.tangem.common.ui.account.toUM
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.decompose.model.Model
@ -15,21 +14,20 @@ import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.ToastMessage
import com.tangem.core.ui.utils.showErrorDialog
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.models.ArchivedAccount
import com.tangem.domain.account.usecase.ArchivedAccountList
import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.GetArchivedAccountsUseCase
import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.account.AccountId
import com.tangem.features.account.ArchivedAccountListComponent
import com.tangem.features.account.archived.entity.AccountArchivedUM
import com.tangem.features.account.archived.entity.AccountArchivedUMBuilder
import com.tangem.features.account.archived.entity.AccountArchivedUMBuilder.Companion.toggleProgress
import com.tangem.features.account.createedit.error.AccountFeatureError
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@ -61,25 +59,32 @@ internal class ArchivedAccountListModel @Inject constructor(
.conflate()
.distinctUntilChanged()
.onEach { lce ->
val newState = when (lce) {
is Lce.Content<ArchivedAccountList> -> umBuilder.mapContent(
accounts = lce.content,
onCloseClick = onCloseClick,
confirmRecoverDialog = { confirmRecoverDialog(it) },
)
is Lce.Error<Throwable> -> umBuilder.mapError(
throwable = lce.error,
onCloseClick = onCloseClick,
getArchivedAccounts = { getArchivedAccounts() },
)
is Lce.Loading<ArchivedAccountList> -> lce.partialContent?.let { content ->
val newState = lce.fold(
ifLoading = { content ->
content ?: return@fold null
umBuilder.mapContent(
accounts = content,
onCloseClick = onCloseClick,
confirmRecoverDialog = { confirmRecoverDialog(it) },
onRecoverClick = { recoverCryptoPortfolio(accountId = it.accountId) },
)
}
}
},
ifContent = { content ->
umBuilder.mapContent(
accounts = content,
onCloseClick = onCloseClick,
onRecoverClick = { recoverCryptoPortfolio(accountId = it.accountId) },
)
},
ifError = { error ->
umBuilder.mapError(
throwable = error,
onCloseClick = onCloseClick,
getArchivedAccounts = { getArchivedAccounts() },
)
},
)
newState?.let { _uiState.value = newState }
}
.flowOn(dispatchers.default)
@ -87,30 +92,13 @@ internal class ArchivedAccountListModel @Inject constructor(
.saveIn(getArchivedAccountsJob)
}
private fun confirmRecoverDialog(account: ArchivedAccount) {
val secondAction = EventMessageAction(
title = resourceReference(R.string.common_cancel),
onClick = {},
)
val firstAction = EventMessageAction(
title = resourceReference(R.string.account_archived_recover),
onClick = { recoverCryptoPortfolio(account.accountId) },
)
messageSender.send(
DialogMessage(
title = resourceReference(R.string.account_archived_recover_dialog_title),
message = resourceReference(
id = R.string.account_archived_recover_dialog_description,
formatArgs = wrappedList(account.name.toUM().value),
),
firstActionBuilder = { firstAction },
secondActionBuilder = { secondAction },
),
)
}
private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch(dispatchers.default) {
recoverCryptoPortfolioUseCase(accountId)
private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch {
_uiState.update { it.toggleProgress(accountId, isLoading = true) }
val result = withContext(dispatchers.default) {
recoverCryptoPortfolioUseCase(accountId)
}
_uiState.update { it.toggleProgress(accountId, isLoading = false) }
result
.onLeft(::handleRecoverError)
.onRight {
showSuccessRecoverMessage()
@ -122,8 +110,22 @@ internal class ArchivedAccountListModel @Inject constructor(
if (error is RecoverCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet &&
error.cause is AccountList.Error.ExceedsMaxAccountsCount
) {
// TODO("account") show alert that max accounts count reached
// https://www.figma.com/design/09KKG4ZVuFDZhj8WLv5rGJ/%F0%9F%9A%A7-App-experience?node-id=24765-180563&t=vk6TCy4MkYol1cPb-4
val firstAction = EventMessageAction(
title = resourceReference(R.string.common_got_it),
onClick = { },
)
messageSender.send(
DialogMessage(
title = resourceReference(R.string.account_recover_limit_dialog_title),
message = resourceReference(
id = R.string.account_recover_limit_dialog_description,
formatArgs = wrappedList(AccountList.MAX_ACCOUNTS_COUNT.toString()),
),
firstActionBuilder = { firstAction },
),
)
return
}

View file

@ -1,9 +1,11 @@
package com.tangem.features.account.archived.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed interface AccountArchivedUM {
val onCloseClick: () -> Unit
@ -24,5 +26,6 @@ internal data class ArchivedAccountUM(
val accountIconUM: CryptoPortfolioIconUM,
val tokensInfo: TextReference,
val networksInfo: TextReference,
val isLoading: Boolean,
val onClick: () -> Unit,
)

View file

@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.account.models.ArchivedAccount
import com.tangem.domain.account.usecase.ArchivedAccountList
import com.tangem.domain.models.account.AccountId
import kotlinx.collections.immutable.toImmutableList
import timber.log.Timber
import javax.inject.Inject
@ -15,19 +16,20 @@ internal class AccountArchivedUMBuilder @Inject constructor() {
fun mapContent(
accounts: ArchivedAccountList,
onCloseClick: () -> Unit,
confirmRecoverDialog: (account: ArchivedAccount) -> Unit,
onRecoverClick: (account: ArchivedAccount) -> Unit,
) = AccountArchivedUM.Content(
onCloseClick = onCloseClick,
accounts = accounts
.map { account -> account.mapArchivedAccountUM(confirmRecoverDialog) }
.map { account -> account.mapArchivedAccountUM(onRecoverClick) }
.toImmutableList(),
)
fun ArchivedAccount.mapArchivedAccountUM(confirmRecoverDialog: (account: ArchivedAccount) -> Unit) =
private fun ArchivedAccount.mapArchivedAccountUM(onRecoverClick: (account: ArchivedAccount) -> Unit) =
ArchivedAccountUM(
accountId = accountId.value,
accountName = name.toUM().value,
accountIconUM = icon.toUM(),
isLoading = false,
tokensInfo = pluralReference(
R.plurals.common_tokens_count,
count = tokensCount,
@ -38,7 +40,7 @@ internal class AccountArchivedUMBuilder @Inject constructor() {
count = networksCount,
formatArgs = wrappedList(networksCount),
),
onClick = { confirmRecoverDialog(this) },
onClick = { onRecoverClick(this) },
)
fun mapError(
@ -52,4 +54,25 @@ internal class AccountArchivedUMBuilder @Inject constructor() {
onRetryClick = { getArchivedAccounts() },
)
}
companion object {
fun AccountArchivedUM.toggleProgress(accountId: AccountId, isLoading: Boolean): AccountArchivedUM {
return when (this) {
is AccountArchivedUM.Error,
is AccountArchivedUM.Loading,
-> this
is AccountArchivedUM.Content -> copy(
accounts = accounts.map { accountUM ->
if (accountUM.accountId == accountId.value) {
accountUM.copy(isLoading = isLoading)
} else {
accountUM
}
}.toImmutableList(),
)
}
}
}
}

View file

@ -126,7 +126,7 @@ private fun ArchivedAccountRow(item: ArchivedAccountUM, modifier: Modifier = Mod
Row(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = item.onClick)
.clickable(enabled = !item.isLoading, onClick = item.onClick)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
@ -148,6 +148,7 @@ private fun ArchivedAccountRow(item: ArchivedAccountUM, modifier: Modifier = Mod
SecondarySmallButton(
config = SmallButtonConfig(
text = resourceReference(R.string.account_archived_recover),
isLoading = item.isLoading,
onClick = item.onClick,
),
)
@ -177,7 +178,7 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountA
tokensInfo = stringReference("10 tokens"),
networksInfo = stringReference("2 networks"),
onClick = {},
isLoading = it % 2 == 0,
)
}.toImmutableList()
val first = AccountArchivedUM.Content(

View file

@ -205,8 +205,8 @@ internal class AccountCreateEditModel @Inject constructor(
is AccountCreateEditComponent.Params.Edit -> {
val oldName = params.account.accountName.toUM()
val isNewName = this.account.name != oldName
val isNewIcon = this.account.portfolioIcon != params.account.portfolioIcon
val isNewName = this.account.name.trim() != oldName
val isNewIcon = this.account.portfolioIcon != params.account.portfolioIcon.toUM()
isValidName && (isNewName || isNewIcon)
}
}
@ -275,4 +275,9 @@ internal class AccountCreateEditModel @Inject constructor(
)
messageSender.send(dialogMessage)
}
}
private fun AccountNameUM.trim(): AccountNameUM = when (this) {
is AccountNameUM.Custom -> this.toDomain().getOrNull()?.toUM() ?: this
AccountNameUM.DefaultMain -> this
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.account.createedit.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.core.ui.extensions.TextReference
@ -23,6 +24,7 @@ internal data class AccountCreateEditUM(
val onNameChange: (AccountNameUM) -> Unit,
)
@Immutable
sealed interface DerivationInfo {
val text: TextReference
val index: Int?

View file

@ -1,9 +1,9 @@
package com.tangem.features.account.createedit.error
import com.tangem.core.error.UniversalError
import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase
import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase
sealed interface AccountFeatureError : UniversalError {

View file

@ -12,6 +12,8 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.Account
@ -20,12 +22,13 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.account.AccountDetailsComponent
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon
import com.tangem.features.account.details.entity.AccountDetailsUM
import com.tangem.features.account.details.entity.AccountDetailsUM.ArchiveMode
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class AccountDetailsModel @Inject constructor(
paramsContainer: ParamsContainer,
@ -33,22 +36,30 @@ internal class AccountDetailsModel @Inject constructor(
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase,
singleAccountSupplier: SingleAccountSupplier,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model() {
private val params = paramsContainer.require<AccountDetailsComponent.Params>()
val uiState: StateFlow<AccountDetailsUM> get() = _uiState
private val _uiState: MutableStateFlow<AccountDetailsUM> = MutableStateFlow(getInitialState())
private val _uiState: MutableStateFlow<AccountDetailsUM> = MutableStateFlow(buildUI(params.account))
private val accountId = params.account.accountId
private fun onEditAccountClick() {
router.push(AppRoute.EditAccount(params.account))
init {
singleAccountSupplier(SingleAccountProducer.Params(accountId))
.onEach { account -> _uiState.update { buildUI(account) } }
.launchIn(modelScope)
}
private fun onManageTokensClick() {
private fun onEditAccountClick(account: Account) {
router.push(AppRoute.EditAccount(account))
}
private fun onManageTokensClick(account: Account) {
val route = AppRoute.ManageTokens(
source = AppRoute.ManageTokens.Source.SETTINGS,
portfolioId = PortfolioId(params.account.accountId),
portfolioId = PortfolioId(account.accountId),
)
router.push(route)
}
@ -78,7 +89,12 @@ internal class AccountDetailsModel @Inject constructor(
}
private fun archiveCryptoPortfolio() = modelScope.launch {
archiveCryptoPortfolioUseCase(params.account.accountId)
_uiState.update { it.toggleProgress(true) }
archiveCryptoPortfolioUseCase(accountId)
.onLeft { error ->
failedArchiveDialog(error)
_uiState.update { it.toggleProgress(false) }
}
.onRight {
val message = resourceReference(R.string.account_archive_success_message)
messageSender.send(ToastMessage(message = message))
@ -86,26 +102,55 @@ internal class AccountDetailsModel @Inject constructor(
}
}
private fun getInitialState(): AccountDetailsUM {
val account = params.account
private fun failedArchiveDialog(error: ArchiveCryptoPortfolioUseCase.Error) {
// todo account referral case
val titleRes = when (error) {
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet,
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound,
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated,
is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed,
-> R.string.common_something_went_wrong
}
val messageRes = when (error) {
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet,
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound,
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated,
is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed,
-> R.string.account_could_not_archive
}
val dialogMessage = DialogMessage(
title = resourceReference(titleRes),
message = resourceReference(messageRes),
)
messageSender.send(dialogMessage)
}
private fun buildUI(account: Account): AccountDetailsUM {
val archiveMode = when (account) {
is Account.CryptoPortfolio -> when (account.isMainAccount) {
true -> AccountDetailsUM.ArchiveMode.None
false -> AccountDetailsUM.ArchiveMode.Available(
true -> ArchiveMode.None
false -> ArchiveMode.Available(
onArchiveAccountClick = ::onArchiveAccountClick,
isLoading = false,
)
}
}
val isMultiCurrency = getUserWalletUseCase(params.account.accountId.userWalletId)
val isMultiCurrency = getUserWalletUseCase(account.accountId.userWalletId)
.getOrNull()?.isMultiCurrency ?: false
return AccountDetailsUM(
accountName = params.account.accountName.toUM().value,
accountIcon = params.account.portfolioIcon.toUM(),
accountName = account.accountName.toUM().value,
accountIcon = account.portfolioIcon.toUM(),
onCloseClick = { router.pop() },
onAccountEditClick = ::onEditAccountClick,
onManageTokensClick = ::onManageTokensClick,
onAccountEditClick = { onEditAccountClick(account) },
onManageTokensClick = { onManageTokensClick(account) },
archiveMode = archiveMode,
isManageTokensAvailable = isMultiCurrency,
)
}
private fun AccountDetailsUM.toggleProgress(isLoading: Boolean): AccountDetailsUM {
val archiveMode = this.archiveMode as? ArchiveMode.Available ?: return this
return this.copy(archiveMode = archiveMode.copy(isLoading = isLoading))
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.account.details.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.core.ui.extensions.TextReference
@ -13,10 +14,12 @@ internal data class AccountDetailsUM(
val onManageTokensClick: () -> Unit,
) {
@Immutable
sealed interface ArchiveMode {
data object None : ArchiveMode
data class Available(
val onArchiveAccountClick: () -> Unit,
val isLoading: Boolean,
) : ArchiveMode
}
}

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -91,21 +92,33 @@ private fun ArchiveAccountRow(state: AccountDetailsUM.ArchiveMode.Available) {
.fillMaxWidth()
.clip(RoundedCornerShape(TangemTheme.dimens.radius12))
.background(TangemTheme.colors.background.primary)
.clickable(onClick = state.onArchiveAccountClick)
.clickable(enabled = !state.isLoading, onClick = state.onArchiveAccountClick)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Icon(
tint = TangemTheme.colors.icon.warning,
imageVector = ImageVector.vectorResource(id = R.drawable.ic_archive_24),
contentDescription = null,
)
Text(
text = stringResourceSafe(R.string.account_details_archive),
color = TangemTheme.colors.text.warning,
style = TangemTheme.typography.subtitle1,
)
if (state.isLoading) {
CircularProgressIndicator(
color = TangemTheme.colors.text.disabled,
modifier = Modifier.size(TangemTheme.dimens.size24),
)
Text(
text = stringResourceSafe(R.string.account_details_archive),
color = TangemTheme.colors.text.disabled,
style = TangemTheme.typography.subtitle1,
)
} else {
Icon(
tint = TangemTheme.colors.icon.warning,
imageVector = ImageVector.vectorResource(id = R.drawable.ic_archive_24),
contentDescription = null,
)
Text(
text = stringResourceSafe(R.string.account_details_archive),
color = TangemTheme.colors.text.warning,
style = TangemTheme.typography.subtitle1,
)
}
}
}
@ -174,6 +187,12 @@ private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::
}
}
private val archiveModeAvailable
get() = AccountDetailsUM.ArchiveMode.Available(
onArchiveAccountClick = {},
isLoading = false,
)
private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountDetailsUM>(
buildList {
val accountName = "Main"
@ -182,16 +201,13 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountD
onCloseClick = {},
onAccountEditClick = {},
onManageTokensClick = {},
archiveMode = AccountDetailsUM.ArchiveMode.Available(
onArchiveAccountClick = {},
),
archiveMode = archiveModeAvailable,
accountName = stringReference(accountName),
accountIcon = portfolioIcon,
isManageTokensAvailable = true,
)
add(first)
portfolioIcon = AccountIconPreviewData.randomAccountIcon(letter = true)
add(first.copy(accountIcon = portfolioIcon))
add(first.copy(archiveMode = archiveModeAvailable.copy(isLoading = true)))
add(first.copy(archiveMode = AccountDetailsUM.ArchiveMode.None))
add(first.copy(isManageTokensAvailable = false))
},

View file

@ -6,6 +6,7 @@ import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.account.PortfolioFetcher
@ -41,7 +42,10 @@ internal class DefaultPortfolioFetcher @AssistedInject constructor(
get() = _mode
init {
_mode.flatMapLatest(::combineUseCases)
_mode
// reset cache if mode(StateFlow) changed
.onEach { _data.resetReplayCache() }
.flatMapLatest(::combineUseCases)
.flowOn(dispatchers.default)
.onEach { _data.emit(it) }
.launchIn(scope)
@ -73,13 +77,13 @@ internal class DefaultPortfolioFetcher @AssistedInject constructor(
}
}
private fun balancesForWallets(wallets: List<UserWallet>): Flow<Map<UserWallet, PortfolioBalance>> {
private fun balancesForWallets(wallets: List<UserWallet>): Flow<Map<UserWalletId, PortfolioBalance>> {
val balanceFlows = wallets.map { walletAccountsBalancesFlow(it) }
return combine(balanceFlows) { pairs -> pairs.toMap() }
}
private fun walletAccountsBalancesFlow(wallet: UserWallet): Flow<Pair<UserWallet, PortfolioBalance>> =
accountStatusListFlow(wallet).map { wallet to PortfolioBalance(wallet, it) }
private fun walletAccountsBalancesFlow(wallet: UserWallet): Flow<Pair<UserWalletId, PortfolioBalance>> =
accountStatusListFlow(wallet).map { wallet.walletId to PortfolioBalance(wallet, it) }
private fun accountStatusListFlow(wallet: UserWallet): Flow<AccountStatusList> =
singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(wallet.walletId))

View file

@ -44,7 +44,7 @@ internal class DefaultPortfolioSelectorController @Inject constructor(
data.balances.forEach { wallet, balance ->
val accountStatuses = balance.accountsBalance.accountStatuses
.find { accountId == it.account.accountId }
if (accountStatuses != null) result = wallet to accountStatuses
if (accountStatuses != null) result = balance.userWallet to accountStatuses
}
return@combine result

View file

@ -10,6 +10,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -42,13 +43,17 @@ internal class PortfolioSelectorModel @Inject constructor(
field = MutableStateFlow<PortfolioSelectorUM>(emptyState())
init {
val selectedAccountState = selectorController.selectedAccount
.stateIn(modelScope, started = SharingStarted.Eagerly, initialValue = null)
combine(
flow = isAccountsModeEnabledUseCase(),
flow2 = balanceFetcher.data,
flow3 = walletImageFetcher.allWallets(ArtworkSize.SMALL),
flow4 = selectorController.isEnabled,
transform = { isAccountsMode, portfolioData, artworks, isEnabled ->
val uiList = buildUiList(isAccountsMode, portfolioData, artworks, isEnabled)
flow5 = selectedAccountState,
transform = { isAccountsMode, portfolioData, artworks, isEnabled, selectedAccount ->
val uiList = buildUiList(isAccountsMode, portfolioData, artworks, isEnabled, selectedAccount)
val title = when (isAccountsMode) {
true -> resourceReference(R.string.common_choose_account)
false -> resourceReference(R.string.common_choose_wallet)
@ -68,21 +73,24 @@ internal class PortfolioSelectorModel @Inject constructor(
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
isEnabled: (UserWallet, AccountStatus) -> Boolean,
selectedAccount: AccountId?,
): List<PortfolioSelectorItemUM> = when (isAccountsMode) {
true -> buildAccountsList(portfolioData, artworks, isEnabled)
false -> buildWalletList(portfolioData, artworks, isEnabled)
true -> buildAccountsList(portfolioData, artworks, isEnabled, selectedAccount)
false -> buildWalletList(portfolioData, artworks, isEnabled, selectedAccount)
}
private fun buildWalletList(
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
isEnabled: (UserWallet, AccountStatus) -> Boolean,
selectedAccount: AccountId?,
): List<PortfolioSelectorItemUM> = buildList {
val appCurrency = portfolioData.appCurrency
val isBalanceHidden = portfolioData.isBalanceHidden
val lockedWallets = mutableListOf<PortfolioSelectorItemUM>()
portfolioData.balances.forEach { wallet, portfolio ->
portfolioData.balances.forEach { walletId, portfolio ->
val balance = portfolio.walletBalance
val wallet = portfolio.userWallet
val walletItemUM = UserWalletItemUMConverter(
onClick = {
selectorController.selectAccount(portfolio.accountsBalance.mainAccount.account.accountId)
@ -94,12 +102,14 @@ internal class PortfolioSelectorModel @Inject constructor(
isAuthMode = false,
).convert(wallet)
if (walletItemUM.isEnabled) {
val isEnabledByFeature = isEnabled(wallet, portfolio.accountsBalance.mainAccount)
val mainAccount = portfolio.accountsBalance.mainAccount
val isEnabledByFeature = isEnabled(wallet, mainAccount)
val finalWalletItemUM =
if (isEnabledByFeature) walletItemUM else walletItemUM.copy(isEnabled = false)
add(PortfolioSelectorItemUM.Portfolio(finalWalletItemUM))
val isSelected = mainAccount.isSelected(selectedAccount)
add(PortfolioSelectorItemUM.Portfolio(finalWalletItemUM, isSelected))
} else {
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM, isSelected = false))
}
}
if (lockedWallets.isNotEmpty()) {
@ -116,12 +126,14 @@ internal class PortfolioSelectorModel @Inject constructor(
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
isEnabled: (UserWallet, AccountStatus) -> Boolean,
selectedAccount: AccountId?,
): List<PortfolioSelectorItemUM> = buildList {
val appCurrency = portfolioData.appCurrency
val isBalanceHidden = portfolioData.isBalanceHidden
val lockedWallets = mutableListOf<PortfolioSelectorItemUM>()
portfolioData.balances.forEach { wallet, portfolio ->
portfolioData.balances.forEach { walletId, portfolio ->
val balance = portfolio.walletBalance
val wallet = portfolio.userWallet
val walletItemUM = UserWalletItemUMConverter(
onClick = {
selectorController.selectAccount(portfolio.accountsBalance.mainAccount.account.accountId)
@ -133,15 +145,21 @@ internal class PortfolioSelectorModel @Inject constructor(
isAuthMode = false,
).convert(wallet)
if (!walletItemUM.isEnabled) {
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM, false))
return@forEach
}
val walletTitle = PortfolioSelectorItemUM.GroupTitle(
id = "GroupTitle ${wallet.walletId.stringValue}",
name = stringReference(wallet.name),
)
add(walletTitle)
when (balanceFetcher.mode.value) {
// for Wallet mode expected single portfolioData.balances
is PortfolioFetcher.Mode.Wallet -> Unit
is PortfolioFetcher.Mode.All -> {
val walletTitle = PortfolioSelectorItemUM.GroupTitle(
id = "GroupTitle ${wallet.walletId.stringValue}",
name = stringReference(wallet.name),
)
add(walletTitle)
}
}
portfolio.accountsBalance.accountStatuses.forEach { accountStatus ->
val isEnabledByFeature = isEnabled(wallet, accountStatus)
@ -156,7 +174,8 @@ internal class PortfolioSelectorModel @Inject constructor(
isEnabled = isEnabledByFeature,
isBalanceHidden = isBalanceHidden,
).convert(account)
add(PortfolioSelectorItemUM.Portfolio(accountItemUM))
val isSelected = accountStatus.isSelected(selectedAccount)
add(PortfolioSelectorItemUM.Portfolio(accountItemUM, isSelected))
}
}
if (lockedWallets.isNotEmpty()) {
@ -169,6 +188,8 @@ internal class PortfolioSelectorModel @Inject constructor(
}
}
private fun AccountStatus.isSelected(selectedId: AccountId?) = this.account.accountId == selectedId
private fun emptyState() = PortfolioSelectorUM(
items = persistentListOf(),
title = TextReference.EMPTY,

View file

@ -0,0 +1,20 @@
package com.tangem.features.account.selector.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.account.selector.PortfolioSelectorModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface PortfolioSelectorModule {
@Binds
@IntoMap
@ClassKey(PortfolioSelectorModel::class)
fun portfolioSelectorModel(model: PortfolioSelectorModel): Model
}

View file

@ -21,7 +21,8 @@ sealed interface PortfolioSelectorItemUM {
data class Portfolio(
val item: UserWalletItemUM,
val isSelected: Boolean,
) : PortfolioSelectorItemUM {
override val id: String = item.id.stringValue
override val id: String = item.id
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.features.account.selector.ui
import android.content.res.Configuration
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
@ -19,6 +21,7 @@ import androidx.compose.ui.text.style.TextOverflow
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.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.userwallet.UserWalletItemRow
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
@ -29,7 +32,6 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.account.impl.R
import com.tangem.features.account.selector.entity.PortfolioSelectorItemUM
import com.tangem.features.account.selector.entity.PortfolioSelectorUM
@ -68,14 +70,21 @@ internal fun PortfolioSelectorContent(
)
}
val portfolioShape = RoundedCornerShape(TangemTheme.dimens.radius14)
val border = BorderStroke(
width = 1.dp,
color = TangemTheme.colors.text.accent,
)
when (item) {
is PortfolioSelectorItemUM.Portfolio -> UserWalletItemRow(
state = item.item,
modifier = offsetModifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size68)
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.clip(portfolioShape)
.background(TangemTheme.colors.background.action)
.conditional(item.isSelected) { border(border, portfolioShape) }
.clickable(enabled = item.item.isEnabled, onClick = item.item.onClick)
.padding(all = TangemTheme.dimens.spacing12)
.conditional(!item.item.isEnabled) { alpha(DISABLED_WALLET_ALPHA) },
@ -122,7 +131,7 @@ internal object PortfolioSelectorPreviewData {
private val accountItem: UserWalletItemUM
get() = UserWalletItemUM(
id = UserWalletId(UUID.randomUUID().toString().encodeToByteArray()),
id = UUID.randomUUID().toString(),
name = accountName,
information = UserWalletItemUM.Information.Loaded(stringReference("12 tokens")),
balance = UserWalletItemUM.Balance.Loaded("$726.04", false),
@ -139,7 +148,7 @@ internal object PortfolioSelectorPreviewData {
private val walletItem: UserWalletItemUM
get() = UserWalletItemUM(
id = UserWalletId(UUID.randomUUID().toString().encodeToByteArray()),
id = UUID.randomUUID().toString(),
name = walletName,
information = UserWalletItemUM.Information.Loaded(stringReference("12 tokens")),
balance = UserWalletItemUM.Balance.Loaded("$726.04", false),
@ -158,16 +167,16 @@ internal object PortfolioSelectorPreviewData {
name = stringReference("Tangem 2.0"),
).let(::add)
accountItem
.let { PortfolioSelectorItemUM.Portfolio(it) }
.let { PortfolioSelectorItemUM.Portfolio(it, false) }
.let(::add)
lockedAccountItem
.let { PortfolioSelectorItemUM.Portfolio(it) }
.let { PortfolioSelectorItemUM.Portfolio(it, false) }
.let(::add)
PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = stringReference("Tangem White"),
).let(::add)
accountItem.let { PortfolioSelectorItemUM.Portfolio(it) }
accountItem.let { PortfolioSelectorItemUM.Portfolio(it, true) }
.let(::add)
}
@ -177,26 +186,26 @@ internal object PortfolioSelectorPreviewData {
id = UUID.randomUUID().toString(),
name = resourceReference(R.string.common_locked_wallets),
).let(::add)
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem, false))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem, false))
}
val walletList
get() = buildList {
add(PortfolioSelectorItemUM.Portfolio(walletItem))
add(PortfolioSelectorItemUM.Portfolio(walletItem))
add(PortfolioSelectorItemUM.Portfolio(walletItem, false))
add(PortfolioSelectorItemUM.Portfolio(walletItem, true))
}
val lockedWalletList
get() = buildList {
add(PortfolioSelectorItemUM.Portfolio(walletItem))
add(PortfolioSelectorItemUM.Portfolio(walletItem, true))
val title = PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = resourceReference(R.string.common_locked_wallets),
)
add(title)
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem, false))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem, false))
}
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:AskBiometryUM.kt$AskBiometryUM$val bottomSheetVariant: Boolean = false</ID>
<ID>BooleanPropertyNaming:AskBiometryUM.kt$AskBiometryUM$val showProgress: Boolean = false</ID>
<ID>BooleanPropertyNaming:DefaultAskBiometryComponent.kt$DefaultAskBiometryComponent$val bsShown by bsShown.collectAsStateWithLifecycle()</ID>
<ID>MultilineLambdaItParameter:AskBiometryModel.kt$AskBiometryModel${ uiMessageSender.send( SnackbarMessage(stringReference("Something went wrong. Please contact support: $it")), ) }</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -114,6 +114,7 @@ internal class AskBiometryModel @Inject constructor(
if (hotWalletFeatureToggles.isHotWalletEnabled) {
walletsRepository.setUseBiometricAuthentication(value = true)
walletsRepository.setRequireAccessCode(value = false)
setBiometryLockForAllWallets()
cardSdkConfigRepository.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = walletsRepository.requireAccessCode().not(),
@ -135,13 +136,15 @@ internal class AskBiometryModel @Inject constructor(
params.modelCallbacks.onAllowed()
}
private fun setBiometryLockForAllWallets() {
modelScope.launch {
userWalletsListRepository.userWalletsSync().forEach { userWallet ->
userWalletsListRepository.setLock(
userWalletId = userWallet.walletId,
lockMethod = UserWalletsListRepository.LockMethod.Biometric,
changeUnsecured = false,
private suspend fun setBiometryLockForAllWallets() {
userWalletsListRepository.userWalletsSync().forEach { userWallet ->
userWalletsListRepository.setLock(
userWalletId = userWallet.walletId,
lockMethod = UserWalletsListRepository.LockMethod.Biometric,
changeUnsecured = false,
).onLeft {
uiMessageSender.send(
SnackbarMessage(stringReference("Something went wrong. Please contact support: $it")),
)
}
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:CreateWalletSelectionUM.kt$CreateWalletSelectionUM$val showAlreadyHaveWallet: Boolean = false</ID>
<ID>MultilineLambdaItParameter:CreateWalletSelectionContent.kt${ Feature( feature = it, modifier = Modifier .padding(top = 12.dp), ) }</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -94,7 +94,7 @@ internal class CreateWalletSelectionModel @Inject constructor(
}
private fun onHardwareWalletClick() {
// TODO [REDACTED_TASK_KEY]
router.push(AppRoute.CreateHardwareWallet)
}
private fun onBuyClick() {

View file

@ -0,0 +1,9 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:CreateWalletStartUM.kt$CreateWalletStartUM$val showScanSecondaryButton: Boolean</ID>
<ID>MultilineLambdaItParameter:CreateWalletStartContent.kt${ FeatureItem( iconResId = it.iconResId, text = it.text, ) }</ID>
<ID>MultilineLambdaItParameter:CreateWalletStartModel.kt$CreateWalletStartModel${ delay(HIDE_PROGRESS_DELAY) setLoading(false) when (it) { is SaveWalletError.DataError -&gt; Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -&gt; { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), ).onRight { appRouter.replaceAll(AppRoute.Wallet) } } } }</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>HasPlatformType:DetailsModel.kt$DetailsModel.Companion$val APP_LANGUAGE = Locale.getDefault().language</ID>
<ID>HasPlatformType:DetailsModel.kt$DetailsModel.Companion$val SYSTEM_LANGUAGE = runCatching { Resources.getSystem().configuration.locales[0].language }.getOrElse { "" }</ID>
<ID>MultilineLambdaItParameter:DetailsModel.kt$DetailsModel${ Timber.w("Unable to check WalletConnect availability: $it") false }</ID>
<ID>MultilineLambdaItParameter:DetailsModel.kt$DetailsModel${ it.copy( selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig( isShown = true, onDismissRequest = { state.update { it.copy( selectFeedbackEmailTypeBSConfig = it.selectFeedbackEmailTypeBSConfig.copy(isShown = false), ) } }, content = SelectEmailFeedbackTypeBS( onOptionClick = { option -&gt; onEmailFeedbackTypeOptionSelected( selectedWalletMetaInfo = selectedWalletMetaInfo, option = option, ) state.update { it.copy( selectFeedbackEmailTypeBSConfig = it.selectFeedbackEmailTypeBSConfig.copy(isShown = false), ) } }, ), ), ) }</ID>
<ID>MultilineLambdaItParameter:DetailsModel.kt$DetailsModel${ it.copy( selectFeedbackEmailTypeBSConfig = it.selectFeedbackEmailTypeBSConfig.copy(isShown = false), ) }</ID>
<ID>MultilineLambdaItParameter:PreviewUserWalletListComponent.kt$PreviewUserWalletListComponent${ it.copy( balance = UserWalletItemUM.Balance.Loaded( value = "1.000 BTC", isFlickering = true, ), ) }</ID>
<ID>MultilineLambdaItParameter:UserWalletSaver.kt$UserWalletSaver${ val message = it.message if (!message.isNullOrEmpty()) { messageSender.send(SnackbarMessage(message)) } }</ID>
<ID>NonBooleanPropertyPrefixedWithIs:UserWalletListModel.kt$UserWalletListModel$private val isWalletSavingInProgress: MutableStateFlow&lt;Boolean&gt; = MutableStateFlow(value = false)</ID>
<ID>RedundantSuspendModifier:UserWalletSaver.kt$UserWalletSaver$suspend</ID>
<ID>UnnecessaryLet:ItemsBuilder.kt$ItemsBuilder$let(::add)</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -10,7 +10,6 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.details.component.UserWalletListComponent
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.impl.R
@ -25,7 +24,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
UserWalletListUM(
userWallets = persistentListOf(
UserWalletItemUM(
id = UserWalletId("user_wallet_1".encodeToByteArray()),
id = "user_wallet_1",
name = stringReference("My Wallet"),
information = getInformation(cardCount = 1),
balance = UserWalletItemUM.Balance.Loading,
@ -33,7 +32,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_2".encodeToByteArray()),
id = "user_wallet_2",
name = stringReference("Old wallet"),
information = getInformation(cardCount = 2),
balance = UserWalletItemUM.Balance.Loading,
@ -41,7 +40,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
id = "user_wallet_3",
name = stringReference("Multi Card"),
information = getInformation(cardCount = 3),
balance = UserWalletItemUM.Balance.Loading,

View file

@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>NullableToStringCall:DisclaimerWebViewClient.kt$DisclaimerWebViewClient$$url</ID>
</CurrentIssues>
</SmellBaseline>

1
features/feed/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,25 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.serialization)
id("kotlin-parcelize")
id("configuration")
}
android {
namespace = "com.tangem.features.feed.api"
}
dependencies {
implementation(deps.compose.foundation)
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Project - Domain */
implementation(projects.domain.core)
implementation(projects.domain.tokens.models)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.markets.models)
}

View file

@ -0,0 +1,6 @@
package com.tangem.features.feed.entry
enum class BottomSheetState {
EXPANDED,
COLLAPSED,
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.feed.entry.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.features.feed.entry.BottomSheetState
@Stable
interface FeedEntryComponent {
@Composable
fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
)
interface Factory {
fun create(context: AppComponentContext): FeedEntryComponent
}
}

View file

@ -0,0 +1,5 @@
package com.tangem.features.feed.entry.featuretoggle
interface FeedFeatureToggle {
val isFeedEnabled: Boolean
}

1
features/feed/impl/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,96 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.features.feed.impl"
}
dependencies {
/* Project - API */
api(projects.features.feed.api)
api(projects.features.onramp.api)
api(projects.features.sendV2.api)
api(projects.features.tokenRecieve.api)
api(projects.features.wallet.api)
api(projects.features.account.api)
/* Data */
implementation(projects.data.common)
/* Domain */
implementation(projects.domain.account)
implementation(projects.domain.account.status)
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.card)
implementation(projects.domain.demo)
implementation(projects.domain.feedback)
implementation(projects.domain.feedback.models)
implementation(projects.domain.manageTokens)
implementation(projects.domain.markets)
implementation(projects.domain.onramp.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.settings)
implementation(projects.domain.notifications.models)
implementation(projects.domain.transaction)
// FIXME [REDACTED_TASK_KEY]
// Remove the "Buy" and "Sell" actions from the redux middleware.
// Instead, create some kind of interface for such cases.
/* Redux -_- */
implementation(projects.domain.legacy)
implementation(deps.reKotlin)
/* Compose */
implementation(deps.compose.coil)
implementation(deps.compose.foundation)
implementation(deps.compose.material3)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.ui.utils)
implementation(deps.lifecycle.compose)
implementation(deps.androidx.activity.compose)
implementation(deps.markdown.composeview)
/* DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/* Other */
implementation(deps.kotlin.immutable.collections)
implementation(deps.timber)
implementation(deps.decompose.ext.compose)
/* Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.core.configToggles)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)
/* Common */
implementation(projects.common.ui)
implementation(projects.common.uiCharts)
implementation(projects.common.routing)
/* Libs */
implementation(projects.libs.crypto)
implementation(projects.libs.blockchainSdk)
/** Tangem libraries */
implementation(tangemDeps.card.core)
implementation(tangemDeps.blockchain)
}

View file

@ -0,0 +1,91 @@
package com.tangem.features.feed.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.popWhile
import com.arkivanov.decompose.value.Value
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
import com.tangem.features.feed.entry.BottomSheetState
import com.tangem.features.feed.entry.components.FeedEntryComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Stable
internal class DefaultFeedEntryComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
private val feedEntryChildFactory: FeedEntryChildFactory,
) : FeedEntryComponent, AppComponentContext by context {
private val stackNavigation = StackNavigation<FeedEntryChildFactory.Child>()
private val innerRouter = InnerRouter<FeedEntryChildFactory.Child>(
stackNavigation = stackNavigation,
popCallback = { onChildBack() },
)
private val stack: Value<ChildStack<FeedEntryChildFactory.Child, Any>> = childStack(
key = "main",
source = stackNavigation,
serializer = FeedEntryChildFactory.Child.serializer(),
initialConfiguration = FeedEntryChildFactory.Child.Feed,
handleBackButton = false,
childFactory = { configuration, factoryContext ->
feedEntryChildFactory.createChild(
child = configuration,
appComponentContext = childByContext(
componentContext = factoryContext,
router = innerRouter,
),
onTokenClick = ::marketsListTokenSelected,
)
},
)
@Composable
override fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
) {
bottomSheetState // TODO will be continued in next tasks.
}
private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) {
innerRouter.push(
route = FeedEntryChildFactory.Child.TokenDetails(
params = DefaultMarketsTokenDetailsComponent.Params(
token = token,
appCurrency = appCurrency,
shouldShowPortfolio = true,
analyticsParams = DefaultMarketsTokenDetailsComponent.AnalyticsParams(
blockchain = null,
source = "Market",
),
),
),
)
}
private fun onChildBack() {
if (stack.value.active.configuration !is FeedEntryChildFactory.Child.Feed) {
stackNavigation.popWhile { it != FeedEntryChildFactory.Child.Feed }
}
}
@AssistedFactory
interface Factory : FeedEntryComponent.Factory {
override fun create(context: AppComponentContext): DefaultFeedEntryComponent
}
}

View file

@ -0,0 +1,77 @@
package com.tangem.features.feed.components
import androidx.compose.runtime.Immutable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.components.feed.DefaultFeedComponent
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
import kotlinx.serialization.Serializable
internal class FeedEntryChildFactory {
@Serializable
@Immutable
sealed interface Child : Route {
@Serializable
@Immutable
data object Feed : Child
@Serializable
@Immutable
data object TokenList : Child
@Serializable
@Immutable
data class TokenDetails(val params: DefaultMarketsTokenDetailsComponent.Params) : Child
@Serializable
@Immutable
data object NewsList : Child
@Serializable
@Immutable
data object NewsDetails : Child
}
fun createChild(
child: Child,
appComponentContext: AppComponentContext,
onTokenClick: (TokenMarketParams, AppCurrency) -> Unit,
): Any {
return when (child) {
is Child.TokenDetails -> {
DefaultMarketsTokenDetailsComponent(
appComponentContext = appComponentContext,
params = child.params,
)
}
is Child.TokenList -> {
DefaultMarketsTokenListComponent(
appComponentContext = appComponentContext,
onTokenClick = onTokenClick,
)
}
Child.NewsDetails -> {
DefaultNewsDetailsComponent(
appComponentContext = appComponentContext,
)
}
Child.NewsList -> {
DefaultNewsListComponent(
appComponentContext = appComponentContext,
)
}
Child.Feed -> {
DefaultFeedComponent(
appComponentContext = appComponentContext,
)
}
}
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.feed.components.feed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableModularContentComponent
internal class DefaultFeedComponent(
appComponentContext: AppComponentContext,
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
@Composable
override fun Title() {
}
@Composable
override fun Content(modifier: Modifier) {
}
@Composable
override fun Footer() {
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.features.feed.components.market.details
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import kotlinx.serialization.Serializable
internal class DefaultMarketsTokenDetailsComponent(
appComponentContext: AppComponentContext,
val params: Params,
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
@Composable
override fun Title() {
}
@Composable
override fun Content(modifier: Modifier) {
}
@Composable
override fun Footer() {
}
@Serializable
data class Params(
val token: TokenMarketParams,
val appCurrency: AppCurrency,
val shouldShowPortfolio: Boolean,
val analyticsParams: AnalyticsParams?,
)
@Serializable
data class AnalyticsParams(
val blockchain: String?,
val source: String,
)
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.feed.components.market.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableModularContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
@Suppress("UnusedPrivateProperty") // TODO will be remove in next PR
internal class DefaultMarketsTokenListComponent(
appComponentContext: AppComponentContext,
private val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit)? = null,
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
@Composable
override fun Title() {
}
@Composable
override fun Content(modifier: Modifier) {
}
@Composable
override fun Footer() {
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.feed.components.news.details
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableModularContentComponent
internal class DefaultNewsDetailsComponent(
appComponentContext: AppComponentContext,
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
@Composable
override fun Title() {
}
@Composable
override fun Content(modifier: Modifier) {
}
@Composable
override fun Footer() {
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.feed.components.news.list
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableModularContentComponent
internal class DefaultNewsListComponent(
appComponentContext: AppComponentContext,
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
@Composable
override fun Title() {
}
@Composable
override fun Content(modifier: Modifier) {
}
@Composable
override fun Footer() {
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.feed.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
import com.tangem.features.feed.featuretoggle.DefaultFeedFeatureToggle
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object FeedFeatureToggleModule {
@Provides
@Singleton
fun provideFeedFeatureToggle(featureTogglesManager: FeatureTogglesManager): FeedFeatureToggle {
return DefaultFeedFeatureToggle(
featureTogglesManager = featureTogglesManager,
)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.feed.featuretoggle
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
internal class DefaultFeedFeatureToggle(
private val featureTogglesManager: FeatureTogglesManager,
) : FeedFeatureToggle {
override val isFeedEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("FEED_ENABLED")
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:HomeButtons.kt$HomeButtonsState$val btnScanStateInProgress: Boolean</ID>
<ID>BooleanPropertyNaming:HomeUM.kt$HomeUM$val scanInProgress: Boolean</ID>
<ID>MultilineLambdaItParameter:HomeModel.kt$HomeModel${ delay(HIDE_PROGRESS_DELAY) setLoading(false) when (it) { is SaveWalletError.DataError -&gt; Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -&gt; appRouter.replaceAll(AppRoute.Wallet) } }</ID>
<ID>MultilineLambdaItParameter:StoriesProgressBar.kt${ when (index) { currentStep -&gt; it.fillMaxWidth(progress.value) in 0 until currentStep -&gt; it.fillMaxWidth(fraction = 1f) else -&gt; it } }</ID>
<ID>NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isFirstStepLaunched = remember { mutableStateOf(false) }</ID>
<ID>NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isLaunched = remember { mutableStateOf(false) }</ID>
<ID>NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isSecondStepLaunched = remember { mutableStateOf(false) }</ID>
<ID>ReusedModifierInstance:HomeButtonsV2.kt$StoriesButton( modifier = modifier, text = stringResourceSafe(id = R.string.common_get_started), useDarkerColors = false, onClick = onGetStartedClick, )</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:CreateWalletBackupComponent.kt$CreateWalletBackupComponent.Params$val setAccessCode: Boolean</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -0,0 +1,9 @@
package com.tangem.features.hotwallet
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface CreateHardwareWalletComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Unit, CreateHardwareWalletComponent>
}

View file

@ -8,6 +8,8 @@ interface CreateWalletBackupComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val isUpgradeFlow: Boolean,
val setAccessCode: 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 ForgetWalletComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
)
interface Factory : ComponentFactory<Params, ForgetWalletComponent>
}

View file

@ -5,6 +5,9 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
interface UpdateAccessCodeComponent : ComposableContentComponent {
data class Params(val userWalletId: UserWalletId)
data class Params(
val userWalletId: UserWalletId,
val isFirstSetup: Boolean,
)
interface Factory : ComponentFactory<Params, UpdateAccessCodeComponent>
}

View file

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

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

@ -14,6 +14,7 @@ android {
dependencies {
/** Api */
implementation(projects.features.hotWallet.api)
implementation(projects.features.onboardingV2.api)
implementation(projects.features.pushNotifications.api)
/** Core modules */
@ -36,6 +37,7 @@ dependencies {
implementation(projects.domain.settings)
implementation(projects.domain.feedback)
implementation(projects.domain.feedback.models)
implementation(projects.domain.hotWallet)
/** Common */
implementation(projects.common.ui)

View file

@ -0,0 +1,72 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:AccessCodeModel.kt$AccessCodeModel$var buttonClicked = false</ID>
<ID>BooleanPropertyNaming:AccessCodeUM.kt$AccessCodeUM$val buttonEnabled: Boolean</ID>
<ID>BooleanPropertyNaming:AccessCodeUM.kt$AccessCodeUM$val buttonInProgress: Boolean</ID>
<ID>BooleanPropertyNaming:AddExistingWalletImportUM.kt$AddExistingWalletImportUM$val importWalletEnabled: Boolean</ID>
<ID>BooleanPropertyNaming:AddExistingWalletImportUM.kt$AddExistingWalletImportUM$val importWalletProgress: Boolean</ID>
<ID>BooleanPropertyNaming:AddExistingWalletImportUM.kt$AddExistingWalletImportUM$val readyToImport: Boolean</ID>
<ID>BooleanPropertyNaming:CreateMobileWalletUM.kt$CreateMobileWalletUM$val createButtonLoading: Boolean</ID>
<ID>BooleanPropertyNaming:ForgetWalletUM.kt$ForgetWalletUM$val firstCheckboxChecked: Boolean</ID>
<ID>BooleanPropertyNaming:ForgetWalletUM.kt$ForgetWalletUM$val secondCheckboxChecked: Boolean</ID>
<ID>BooleanPropertyNaming:HotAccessCodeRequestUM.kt$HotAccessCodeRequestUM$val useBiometricVisible: Boolean = true</ID>
<ID>BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showBackButton: Boolean</ID>
<ID>BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showFeedbackButton: Boolean</ID>
<ID>BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showSkipButton: Boolean</ID>
<ID>BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM$val completeButtonEnabled: Boolean</ID>
<ID>BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM$val completeButtonProgress: Boolean</ID>
<ID>BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM.WordField$val error: Boolean</ID>
<ID>BooleanPropertyNaming:MobileWalletSetupFinishedContent.kt$var showConfetti by remember { mutableStateOf(false) }</ID>
<ID>BooleanPropertyNaming:UpgradeWalletModel.kt$UpgradeWalletModel$val otherWalletAndAlreadyCreated by lazy { userWallet?.walletId != params.userWalletId &amp;&amp; it.card.wallets.map { it.curve }.toSet().isNotEmpty() }</ID>
<ID>BooleanPropertyNaming:UpgradeWalletModel.kt$UpgradeWalletModel$val sameWalletButNotFinishedBackup by lazy { userWallet?.walletId == params.userWalletId &amp;&amp; BackupValidator.isValidFull(it.card).not() }</ID>
<ID>BooleanPropertyNaming:WalletBackupUM.kt$WalletBackupUM$val backedUp: Boolean</ID>
<ID>BooleanPropertyNaming:WalletHardwareBackupUM.kt$WalletHardwareBackupUM$val showPurchaseBlock: Boolean = false</ID>
<ID>MaxChainedCallsOnSameLine:UpgradeWalletModel.kt$UpgradeWalletModel$it.card.wallets.map { it.curve }.toSet().isNotEmpty()</ID>
<ID>MultilineLambdaItParameter:AccessCodeModel.kt$AccessCodeModel${ Timber.e(it) uiState.update { it.copy(buttonInProgress = false) } }</ID>
<ID>MultilineLambdaItParameter:AccessCodeModel.kt$AccessCodeModel${ it.copy( accessCode = value, buttonEnabled = if (params.accessCodeToConfirm != null) { value == params.accessCodeToConfirm } else { value.length == uiState.value.accessCodeLength }, accessCodeColor = when { params.accessCodeToConfirm == null -&gt; PinTextColor.Primary value.length != uiState.value.accessCodeLength -&gt; PinTextColor.Primary value == params.accessCodeToConfirm -&gt; PinTextColor.Primary else -&gt; PinTextColor.WrongCode }, ) }</ID>
<ID>MultilineLambdaItParameter:AddExistingWalletImportModel.kt$AddExistingWalletImportModel${ Timber.e(it) setImportProgress(false) }</ID>
<ID>MultilineLambdaItParameter:AddExistingWalletImportModel.kt$AddExistingWalletImportModel${ setImportProgress(false) when (it) { is SaveWalletError.DataError -&gt; Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -&gt; { uiMessageSender.send( SnackbarMessage(resourceReference(R.string.hw_import_seed_phrase_already_imported)), ) } } }</ID>
<ID>MultilineLambdaItParameter:CreateHardwareWalletModel.kt$CreateHardwareWalletModel${ delay(HIDE_PROGRESS_DELAY) setLoading(false) when (it) { is SaveWalletError.DataError -&gt; Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -&gt; { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), ).onRight { router.replaceAll(AppRoute.Wallet) } } } }</ID>
<ID>MultilineLambdaItParameter:CreateMobileWalletModel.kt$CreateMobileWalletModel${ Timber.e(it) uiState.update { it.copy(createButtonLoading = false) } }</ID>
<ID>MultilineLambdaItParameter:ForgetWalletModel.kt$ForgetWalletModel${ Timber.e("Unable to delete wallet: $it") uiMessageSender.send( message = SnackbarMessage(resourceReference(R.string.common_unknown_error)), ) return@launch }</ID>
<ID>MultilineLambdaItParameter:ForgetWalletModel.kt$ForgetWalletModel${ val newValue = !it.firstCheckboxChecked it.copy( firstCheckboxChecked = newValue, isForgetButtonEnabled = newValue &amp;&amp; it.secondCheckboxChecked, ) }</ID>
<ID>MultilineLambdaItParameter:ForgetWalletModel.kt$ForgetWalletModel${ val newValue = !it.secondCheckboxChecked it.copy( secondCheckboxChecked = newValue, isForgetButtonEnabled = it.firstCheckboxChecked &amp;&amp; newValue, ) }</ID>
<ID>MultilineLambdaItParameter:HotAccessCodeRequestModel.kt$HotAccessCodeRequestModel${ it.copy( accessCode = accessCode, accessCodeColor = PinTextColor.Primary, ) }</ID>
<ID>MultilineLambdaItParameter:HotAccessCodeRequestModel.kt$HotAccessCodeRequestModel${ it.copy( accessCodeColor = PinTextColor.Success, onAccessCodeChange = {}, ) }</ID>
<ID>MultilineLambdaItParameter:HotAccessCodeRequestModel.kt$HotAccessCodeRequestModel${ it.copy( accessCodeColor = PinTextColor.WrongCode, onAccessCodeChange = {}, useBiometricVisible = currentRequest.isBiometryButtonVisible(), ) }</ID>
<ID>MultilineLambdaItParameter:HotAccessCodeRequestModel.kt$HotAccessCodeRequestModel${ it.copy( isShown = true, accessCode = "", useBiometricVisible = attemptRequest.isBiometryButtonVisible(), onAccessCodeChange = ::onAccessCodeChange, ) }</ID>
<ID>MultilineLambdaItParameter:HotAccessCodeRequestModel.kt$HotAccessCodeRequestModel${ it.copy( onAccessCodeChange = {}, useBiometricVisible = false, ) }</ID>
<ID>MultilineLambdaItParameter:HotAccessCodeRequestModel.kt$HotAccessCodeRequestModel${ it.copy( wrongAccessCodeText = null, onAccessCodeChange = ::onAccessCodeChange, ) }</ID>
<ID>MultilineLambdaItParameter:HotAccessCodeRequestModel.kt$HotAccessCodeRequestModel${ it.copy( wrongAccessCodeText = remainingSecondsToText(attempts.remainingSeconds) ?: resourceReference( R.string.access_code_check_warining_delete, wrappedList(attempts.remainingAttemptsCountBeforeDeletion), ), onAccessCodeChange = ::onAccessCodeChange.takeIf { attempts.remainingSeconds &lt;= 0 } ?: {}, ) }</ID>
<ID>MultilineLambdaItParameter:HotAccessCodeRequestModel.kt$HotAccessCodeRequestModel${ it.copy( wrongAccessCodeText = remainingSecondsToText(attempts.remainingSeconds), onAccessCodeChange = ::onAccessCodeChange.takeIf { attempts.remainingSeconds &lt;= 0 } ?: {}, ) }</ID>
<ID>MultilineLambdaItParameter:HotAccessCodeRequestModel.kt$HotAccessCodeRequestModel${ it.copy( wrongAccessCodeText = resourceReference( R.string.access_code_check_warining_lock, wrappedList(MAX_FAST_FORWARD_ATTEMPTS - attempts.count), ), onAccessCodeChange = ::onAccessCodeChange, ) }</ID>
<ID>MultilineLambdaItParameter:ImportSeedPhraseUiStateBuilder.kt$ImportSeedPhraseUiStateBuilder${ it.copy( importWalletEnabled = false, wordsErrorText = null, ) }</ID>
<ID>MultilineLambdaItParameter:ImportSeedPhraseUiStateBuilder.kt$ImportSeedPhraseUiStateBuilder${ it.copy( invalidWords = emptyList&lt;String&gt;().toImmutableList(), wordsErrorText = null, importWalletEnabled = true, ) }</ID>
<ID>MultilineLambdaItParameter:ImportSeedPhraseUiStateBuilder.kt$ImportSeedPhraseUiStateBuilder${ it.copy( invalidWords = invalidWords.toImmutableList(), wordsErrorText = resourceReference(R.string.onboarding_seed_mnemonic_wrong_words), importWalletEnabled = false, ) }</ID>
<ID>MultilineLambdaItParameter:ImportSeedPhraseUiStateBuilder.kt$ImportSeedPhraseUiStateBuilder${ it.copy( wordsErrorText = resourceReference(R.string.onboarding_seed_mnemonic_invalid_checksum), importWalletEnabled = false, ) }</ID>
<ID>MultilineLambdaItParameter:ImportSeedPhraseUiStateBuilder.kt$ImportSeedPhraseUiStateBuilder${ launchInterceptWords(wordsField = it) suggestNextWord(it) updateUiState { state -&gt; state.copy(words = it) } }</ID>
<ID>MultilineLambdaItParameter:ImportSeedPhraseUiStateBuilder.kt$ImportSeedPhraseUiStateBuilder${ passphrase = it.text updateUiState { state -&gt; state.copy(passPhrase = it) } }</ID>
<ID>MultilineLambdaItParameter:InvalidWordsColorTransformation.kt$InvalidWordsColorTransformation${ if (it == WORD_SEPARATOR) { append(WHITE_SPACE) } else if (wordsToBrush.contains(it)) { append(it.annotate()) } else { append(it) } }</ID>
<ID>MultilineLambdaItParameter:ManualBackupCheckModel.kt$ManualBackupCheckModel${ Timber.e(it) uiState.update { it.copy(completeButtonProgress = false) } }</ID>
<ID>MultilineLambdaItParameter:ManualBackupCheckModel.kt$ManualBackupCheckModel${ it.copy( words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.filterIndexed { index, _ -&gt; WORD_FIELD_INDICES.contains(index + 1) }.toImmutableList(), ) }</ID>
<ID>MultilineLambdaItParameter:ManualBackupPhraseContent.kt${ EnumeratedTwoColumnGridItem( index = it + 1, mnemonic = "word${it + 1}", ) }</ID>
<ID>MultilineLambdaItParameter:ManualBackupPhraseModel.kt$ManualBackupPhraseModel${ it.copy( words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.mapIndexed { index, s -&gt; EnumeratedTwoColumnGridItem(index + 1, s) }.toImmutableList(), ) }</ID>
<ID>MultilineLambdaItParameter:UpgradeWalletModel.kt$UpgradeWalletModel${ // Check if user attempted to upgrade before but something went wrong and a full reset is required val userWallet = coldUserWalletBuilderFactory.create(it).build() val sameWalletButNotFinishedBackup by lazy { userWallet?.walletId == params.userWalletId &amp;&amp; BackupValidator.isValidFull(it.card).not() } val otherWalletAndAlreadyCreated by lazy { userWallet?.walletId != params.userWalletId &amp;&amp; it.card.wallets.map { it.curve }.toSet().isNotEmpty() } if (userWallet != null &amp;&amp; (sameWalletButNotFinishedBackup || otherWalletAndAlreadyCreated)) { startResetCardsFlow.emit(userWallet) return@doOnSuccess } delay(DELAY_SDK_DIALOG_CLOSE) tangemSdkManager.changeDisplayedCardIdNumbersCount(it) navigateToUpgradeFlow(it) }</ID>
<ID>MultilineLambdaItParameter:ViewPhraseContent.kt${ EnumeratedTwoColumnGridItem( index = it + 1, mnemonic = "word${it + 1}", ) }</ID>
<ID>MultilineLambdaItParameter:ViewPhraseModel.kt$ViewPhraseModel${ it.copy( words = words.mapIndexed { index, s -&gt; EnumeratedTwoColumnGridItem(index + 1, s) }.toImmutableList(), ) }</ID>
<ID>NoNameShadowing:AccessCodeModel.kt$AccessCodeModel${ it.copy(buttonInProgress = false) }</ID>
<ID>NoNameShadowing:CreateMobileWalletModel.kt$CreateMobileWalletModel${ it.copy(createButtonLoading = false) }</ID>
<ID>NoNameShadowing:ManualBackupCheckModel.kt$ManualBackupCheckModel${ it.copy(completeButtonProgress = false) }</ID>
<ID>PropertyUsedBeforeDeclaration:AddExistingWalletImportModel.kt$AddExistingWalletImportModel$uiState</ID>
<ID>ReusedModifierInstance:AddExistingWalletImportContent.kt$OutlineTextFieldWithIcon( modifier = modifier .padding(horizontal = 16.dp) .fillMaxWidth(), value = state.passPhrase, onValueChange = state.passPhraseChange, iconResId = R.drawable.ic_information_24, iconColor = TangemTheme.colors.icon.informative, label = stringResourceSafe(id = R.string.common_passphrase), placeholder = stringResourceSafe(id = R.string.send_optional_field), onIconClick = state.onPassphraseInfoClick, keyboardOptions = KeyboardOptions( autoCorrectEnabled = false, keyboardType = KeyboardType.Password, ), )</ID>
<ID>ReusedModifierInstance:HotAccessCodeRequestFullScreenContent.kt$AnimatedVisibility( modifier = modifier, visible = state.isShown, enter = fadeIn(), exit = fadeOut(), ) { Column( Modifier .fillMaxSize() .background(TangemTheme.colors.background.primary), horizontalAlignment = Alignment.CenterHorizontally, ) { TangemTopAppBar( modifier = Modifier.statusBarsPadding(), startButton = TopAppBarButtonUM.Back(state.onDismiss), ) SpacerH(68.dp) Column( Modifier .weight(1f) .fillMaxWidth() .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( modifier = Modifier.animateEnterExit( enter = slideInVertically( tween(), initialOffsetY = { it + 200 }, ) + fadeIn(tween()), exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), ), text = stringResourceSafe(R.string.access_code_check_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) SpacerH24() PinTextField( modifier = Modifier.animateEnterExit( enter = slideInVertically( tween(), initialOffsetY = { it + 200 }, ) + fadeIn(tween()), exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), ), length = 6, isPasswordVisual = true, value = state.accessCode, pinTextColor = state.accessCodeColor, onValueChange = state.onAccessCodeChange, ) SpacerH(20.dp) AnimatedVisibility( modifier = Modifier.animateEnterExit( enter = slideInVertically( tween(), initialOffsetY = { it + 200 }, ) + fadeIn(tween()), exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), ), visible = state.wrongAccessCodeText != null, enter = fadeIn(), exit = fadeOut(), ) { val wrongAccessCodeText = state.wrongAccessCodeText ?: return@AnimatedVisibility Text( text = wrongAccessCodeText.resolveReference(), textAlign = TextAlign.Center, style = TangemTheme.typography.caption2.copy( lineBreak = LineBreak.Heading, ), color = TangemTheme.colors.text.warning, ) } } AnimatedVisibility( visible = state.useBiometricVisible, enter = fadeIn(), exit = fadeOut(), ) { SecondaryButton( modifier = Modifier .padding(16.dp) .fillMaxWidth() .navigationBarsPadding() .imePadding(), text = stringResourceSafe( id = R.string.welcome_unlock, stringResourceSafe(R.string.common_biometrics), ), onClick = state.useBiometricClick, ) } } }</ID>
<ID>ReusedModifierInstance:HotWalletStepper.kt$TangemTopAppBar( startButton = if (state.showBackButton) { TopAppBarButtonUM.Back(onBackClick) } else { null }, endButton = when { state.showSkipButton -&gt; TopAppBarButtonUM.Text( text = resourceReference(R.string.common_skip), onClicked = onSkipClick, ) state.showFeedbackButton -&gt; TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_chat_24, onClicked = onFeedbackClick, ) else -&gt; null }, title = state.title, containerColor = TangemTheme.colors.background.primary, modifier = modifier, titleAlignment = Alignment.CenterHorizontally, )</ID>
<ID>SuspendFunSwallowedCancellation:AccessCodeModel.kt$AccessCodeModel$runCatching</ID>
<ID>SuspendFunSwallowedCancellation:AddExistingWalletImportModel.kt$AddExistingWalletImportModel$runCatching</ID>
<ID>SuspendFunSwallowedCancellation:CreateMobileWalletModel.kt$CreateMobileWalletModel$runCatching</ID>
<ID>SuspendFunSwallowedCancellation:ManualBackupCheckModel.kt$ManualBackupCheckModel$runCatching</ID>
<ID>SuspendFunSwallowedCancellation:ManualBackupPhraseModel.kt$ManualBackupPhraseModel$runCatching</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -81,7 +81,7 @@ internal class AccessCodeModel @Inject constructor(
accessCodeColor = when {
params.accessCodeToConfirm == null -> PinTextColor.Primary
value.length != uiState.value.accessCodeLength -> PinTextColor.Primary
value == params.accessCodeToConfirm -> PinTextColor.Success
value == params.accessCodeToConfirm -> PinTextColor.Primary
else -> PinTextColor.WrongCode
},
)

View file

@ -2,6 +2,7 @@ package com.tangem.features.hotwallet.addexistingwallet.entry
import com.arkivanov.decompose.router.stack.*
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
@ -13,6 +14,7 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
@ -32,6 +34,8 @@ internal class AddExistingWalletModel @Inject constructor(
private val router: Router,
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val trackingContextProxy: TrackingContextProxy,
private val setAccessCodeSkippedUseCase: SetAccessCodeSkippedUseCase,
) : Model() {
val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback()
@ -45,6 +49,15 @@ internal class AddExistingWalletModel @Inject constructor(
val startRoute = AddExistingWalletRoute.Import
val currentRoute: MutableStateFlow<AddExistingWalletRoute> = MutableStateFlow(startRoute)
init {
trackingContextProxy.addHotWalletContext()
}
override fun onDestroy() {
super.onDestroy()
trackingContextProxy.removeContext()
}
fun onChildBack() {
when (currentRoute.value) {
is AddExistingWalletRoute.Import -> router.pop()
@ -72,6 +85,12 @@ internal class AddExistingWalletModel @Inject constructor(
}
private fun showSkipAccessCodeWarningDialog() {
val userWalletId = when (val route = currentRoute.value) {
is AddExistingWalletRoute.SetAccessCode -> route.userWalletId
is AddExistingWalletRoute.ConfirmAccessCode -> route.userWalletId
else -> null
}
uiMessageSender.send(
DialogMessage(
message = resourceReference(R.string.access_code_alert_skip_description),
@ -82,7 +101,14 @@ internal class AddExistingWalletModel @Inject constructor(
),
secondAction = EventMessageAction(
title = resourceReference(R.string.access_code_alert_skip_ok),
onClick = { navigateToPushNotificationsOrNext() },
onClick = {
if (userWalletId != null) {
modelScope.launch {
setAccessCodeSkippedUseCase(userWalletId, true)
}
}
navigateToPushNotificationsOrNext()
},
),
shouldDismissOnFirstAction = true,
),
@ -109,6 +135,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 {
@ -137,7 +165,7 @@ internal class AddExistingWalletModel @Inject constructor(
inner class MobileWalletSetupFinishedComponentModelCallbacks :
MobileWalletSetupFinishedComponent.ModelCallbacks {
override fun onContinueClick() {
override fun onFinishClick() {
router.replaceAll(AppRoute.Wallet)
}
}

View file

@ -21,7 +21,7 @@ internal class AddExistingWalletStepperStateManager {
is AddExistingWalletRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_BACKUP,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_backup),
title = resourceReference(R.string.wallet_import_title),
showBackButton = false,
showSkipButton = false,
showFeedbackButton = false,

View file

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

View file

@ -0,0 +1,183 @@
package com.tangem.features.hotwallet.createhardwarewallet
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic.SignedIn
import com.tangem.core.analytics.models.Basic.SignedIn.SignInType
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
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.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.SaveWalletError
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.hotwallet.createhardwarewallet.entity.CreateHardwareWalletUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
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
private const val HIDE_PROGRESS_DELAY = 400L
@Suppress("LongParameterList")
@ModelScoped
internal class CreateHardwareWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener,
private val settingsRepository: SettingsRepository,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val uiMessageSender: UiMessageSender,
private val scanCardProcessor: ScanCardProcessor,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val saveWalletUseCase: SaveWalletUseCase,
private val userWalletsListRepository: UserWalletsListRepository,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
val uiState: StateFlow<CreateHardwareWalletUM>
field = MutableStateFlow(
CreateHardwareWalletUM(
onBackClick = { router.pop() },
onBuyTangemWalletClick = ::onBuyTangemWalletClick,
onScanDeviceClick = ::onScanDeviceClick,
),
)
override fun onDestroy() {
super.onDestroy()
}
private fun onBuyTangemWalletClick() {
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
}
}
private fun onScanDeviceClick() {
scanCard()
}
private fun scanCard() {
modelScope.launch {
setLoading(true)
val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes()
cardSdkConfigRepository.setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = shouldSaveAccessCodes,
)
val analyticsSource = AnalyticsParam.ScreensSources.Intro
scanCardProcessor.scan(
analyticsSource = analyticsSource,
onProgressStateChange = { showProgress ->
if (!showProgress) {
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
} else {
setLoading(true)
}
},
onFailure = { error ->
handleScanError(error)
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
},
onSuccess = { scanResponse ->
proceedWithScanResponse(scanResponse)
},
)
}
}
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
if (userWallet == null) {
Timber.e("User wallet not created")
setLoading(false)
return
}
saveWalletUseCase(userWallet = userWallet).fold(
ifLeft = {
delay(HIDE_PROGRESS_DELAY)
setLoading(false)
when (it) {
is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet")
is SaveWalletError.WalletAlreadySaved -> {
userWalletsListRepository.unlock(
userWalletId = userWallet.walletId,
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse),
).onRight {
router.replaceAll(AppRoute.Wallet)
}
}
}
},
ifRight = {
setLoading(false)
sendSignedInCardAnalyticsEvent(scanResponse = scanResponse, isImported = userWallet.isImported)
router.replaceAll(AppRoute.Wallet)
},
)
}
private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse, isImported: Boolean) {
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
if (currency != null) {
analyticsEventHandler.send(
SignedIn(
currency = currency,
batch = scanResponse.card.batchId,
signInType = SignInType.Card,
walletsCount = userWalletsListRepository.userWalletsSync().size.toString(),
isImported = isImported,
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)
}
}
private fun setLoading(isLoading: Boolean) {
uiState.update { it.copy(isScanInProgress = isLoading) }
}
private fun handleScanError(error: TangemError) {
when (error) {
is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable()
is TangemSdkError -> Timber.e(error, "Scan error occurred")
else -> Timber.e(error, "Error happened")
}
}
private fun handleNfcFeatureUnavailable() {
uiMessageSender.send(
message = DialogMessage(
message = resourceReference(R.string.nfc_error_unavailable),
title = resourceReference(id = R.string.common_error),
),
)
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.features.hotwallet.createhardwarewallet
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.CreateHardwareWalletComponent
import com.tangem.features.hotwallet.createhardwarewallet.ui.CreateHardwareWalletContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember")
internal class DefaultCreateHardwareWalletComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: Unit,
) : CreateHardwareWalletComponent, AppComponentContext by context {
private val model: CreateHardwareWalletModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
CreateHardwareWalletContent(
state = state,
modifier = modifier,
)
}
@AssistedFactory
interface Factory : CreateHardwareWalletComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultCreateHardwareWalletComponent
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.hotwallet.createhardwarewallet.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.CreateHardwareWalletComponent
import com.tangem.features.hotwallet.createhardwarewallet.CreateHardwareWalletModel
import com.tangem.features.hotwallet.createhardwarewallet.DefaultCreateHardwareWalletComponent
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 CreateHardwareWalletModule {
@Binds
fun bindCreateHardwareWalletComponentFactory(
impl: DefaultCreateHardwareWalletComponent.Factory,
): CreateHardwareWalletComponent.Factory
@Binds
@IntoMap
@ClassKey(CreateHardwareWalletModel::class)
fun bindCreateHardwareWalletModel(model: CreateHardwareWalletModel): Model
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.hotwallet.createhardwarewallet.entity
internal data class CreateHardwareWalletUM(
val onBackClick: () -> Unit,
val onBuyTangemWalletClick: () -> Unit,
val onScanDeviceClick: () -> Unit,
val isScanInProgress: Boolean = false,
)

View file

@ -0,0 +1,130 @@
package com.tangem.features.hotwallet.createhardwarewallet.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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
import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.feature.FeatureBlock
import com.tangem.core.ui.extensions.TextReference
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.createhardwarewallet.entity.CreateHardwareWalletUM
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun CreateHardwareWalletContent(state: CreateHardwareWalletUM, modifier: Modifier = Modifier) {
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(
start = 16.dp,
top = 24.dp,
end = 16.dp,
),
) {
Icon(
modifier = Modifier
.fillMaxWidth(),
painter = painterResource(R.drawable.ic_tangem_64),
contentDescription = null,
tint = Color.Unspecified,
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 16.dp,
top = 20.dp,
end = 16.dp,
),
text = stringResourceSafe(R.string.wallet_create_common_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
FeatureBlock(
modifier = Modifier
.padding(top = 32.dp),
title = stringResourceSafe(R.string.hw_upgrade_key_migration_title),
description = stringResourceSafe(R.string.hw_upgrade_key_migration_description),
iconRes = R.drawable.ic_mobile_security_24,
)
FeatureBlock(
modifier = Modifier
.padding(top = 24.dp),
title = stringResourceSafe(R.string.hw_upgrade_funds_access_title),
description = stringResourceSafe(R.string.hw_upgrade_funds_access_description),
iconRes = R.drawable.ic_knight_shield_24,
)
FeatureBlock(
modifier = Modifier
.padding(top = 24.dp),
title = stringResourceSafe(R.string.hw_upgrade_general_security_title),
description = stringResourceSafe(R.string.hw_upgrade_general_security_description),
iconRes = R.drawable.ic_protect_24,
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
SecondaryButton(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(R.string.details_buy_wallet),
onClick = state.onBuyTangemWalletClick,
)
PrimaryButtonIconEnd(
modifier = Modifier
.fillMaxWidth(),
text = stringResourceSafe(R.string.home_button_scan),
onClick = state.onScanDeviceClick,
iconResId = R.drawable.ic_tangem_24,
showProgress = state.isScanInProgress,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewCreateHardwareWalletContent() {
TangemThemePreview {
CreateHardwareWalletContent(
state = CreateHardwareWalletUM(
onBackClick = {},
onBuyTangemWalletClick = {},
onScanDeviceClick = {},
),
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.hotwallet.createmobilewallet
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
@ -25,6 +26,7 @@ internal class CreateMobileWalletModel @Inject constructor(
private val saveUserWalletUseCase: SaveWalletUseCase,
private val router: Router,
private val tangemHotSdk: TangemHotSdk,
private val trackingContextProxy: TrackingContextProxy,
) : Model() {
internal val uiState: StateFlow<CreateMobileWalletUM>
@ -37,6 +39,15 @@ internal class CreateMobileWalletModel @Inject constructor(
),
)
init {
trackingContextProxy.addHotWalletContext()
}
override fun onDestroy() {
super.onDestroy()
trackingContextProxy.removeContext()
}
private fun onImportClick() {
router.push(AppRoute.AddExistingWallet)
}

View file

@ -15,6 +15,7 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.feature.FeatureBlock
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -103,39 +104,6 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo
}
}
@Composable
private fun FeatureBlock(title: String, description: String, iconRes: Int, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
) {
Icon(
modifier = Modifier
.padding(horizontal = 12.dp),
painter = painterResource(iconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
) {
Text(
text = title,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier
.padding(top = 4.dp),
text = description,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable

View file

@ -3,10 +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.TrackingContextProxy
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
@ -23,6 +26,7 @@ internal class CreateWalletBackupModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val trackingContextProxy: TrackingContextProxy,
) : Model() {
val params = paramsContainer.require<CreateWalletBackupComponent.Params>()
@ -36,6 +40,15 @@ internal class CreateWalletBackupModel @Inject constructor(
val startRoute = CreateWalletBackupRoute.RecoveryPhraseStart
val currentRoute: MutableStateFlow<CreateWalletBackupRoute> = MutableStateFlow(startRoute)
init {
trackingContextProxy.addHotWalletContext()
}
override fun onDestroy() {
super.onDestroy()
trackingContextProxy.removeContext()
}
fun onBack() {
when (currentRoute.value) {
is CreateWalletBackupRoute.RecoveryPhraseStart -> router.pop()
@ -54,11 +67,20 @@ internal class CreateWalletBackupModel @Inject constructor(
}
fun onManualBackupChecked() {
stackNavigation.push(CreateWalletBackupRoute.BackupCompleted)
stackNavigation.push(CreateWalletBackupRoute.BackupCompleted(isUpgradeFlow = params.isUpgradeFlow))
}
fun onManualBackupCompleted() {
router.pop()
if (params.setAccessCode) {
router.replaceCurrent(
route = AppRoute.UpdateAccessCode(
userWalletId = params.userWalletId,
isFirstSetup = true,
),
)
} else {
router.pop()
}
}
inner class ManualBackupStartModelCallbacks : ManualBackupStartComponent.ModelCallbacks {
@ -83,5 +105,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

@ -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,112 @@
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.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() {
uiMessageSender.send(
DialogMessage(
title = resourceReference(R.string.common_attention),
message = resourceReference(R.string.hw_remove_wallet_confirmation_title),
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_forget),
isWarning = true,
onClick = ::forgetWallet,
)
},
secondActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_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,154 @@
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.extensions.stringResourceSafe
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) {
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 = stringResourceSafe(R.string.common_attention),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = stringResourceSafe(R.string.hw_remove_wallet_attention_description),
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 = stringResourceSafe(R.string.hw_remove_wallet_warning_device),
)
Spacer(modifier = Modifier.height(16.dp))
CheckboxItem(
modifier = Modifier.padding(horizontal = 16.dp),
checked = state.secondCheckboxChecked,
onCheckedChange = state.onSecondCheckboxClick,
text = stringResourceSafe(R.string.hw_remove_wallet_warning_access),
)
Spacer(modifier = Modifier.height(32.dp))
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = stringResourceSafe(R.string.hw_remove_wallet_action_forget_title),
onClick = state.onForgetWalletClick,
enabled = state.isForgetButtonEnabled,
)
}
}
@Composable
private fun CheckboxItem(checked: Boolean, onCheckedChange: () -> Unit, text: String, modifier: Modifier = Modifier) {
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

@ -29,10 +29,13 @@ 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 isImportFlow: Boolean = false,
val isUpgradeFlow: Boolean = false,
)
}

View file

@ -3,12 +3,15 @@ package com.tangem.features.hotwallet.manualbackup.completed
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.res.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.hotwallet.manualbackup.completed.entity.ManualBackupCompletedUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
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 +23,20 @@ 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)
}
},
title = if (params.isImportFlow) {
resourceReference(R.string.wallet_import_success_title)
} else {
resourceReference(R.string.backup_complete_title)
},
description = resourceReference(R.string.backup_complete_description),
isLoading = false,
),
)
}

View file

@ -1,5 +1,10 @@
package com.tangem.features.hotwallet.manualbackup.completed.entity
import com.tangem.core.ui.extensions.TextReference
internal data class ManualBackupCompletedUM(
val title: TextReference,
val description: TextReference,
val onContinueClick: () -> Unit,
val isLoading: Boolean,
)

View file

@ -16,6 +16,8 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.features.hotwallet.manualbackup.completed.entity.ManualBackupCompletedUM
@ -43,7 +45,7 @@ internal fun ManualBackupCompletedContent(state: ManualBackupCompletedUM, modifi
top = 20.dp,
end = 48.dp,
),
text = stringResourceSafe(R.string.backup_complete_title),
text = state.title.resolveReference(),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
@ -65,8 +67,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 +75,30 @@ 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 PreviewManualBackupCompletedContentBackup() {
TangemThemePreview {
ManualBackupCompletedContent(
state = ManualBackupCompletedUM(
onContinueClick = {},
isLoading = false,
title = resourceReference(R.string.backup_complete_title),
description = resourceReference(R.string.backup_complete_description),
onContinueClick = { },
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewManualBackupCompletedContentImport() {
TangemThemePreview {
ManualBackupCompletedContent(
state = ManualBackupCompletedUM(
isLoading = false,
title = resourceReference(R.string.wallet_import_success_title),
description = resourceReference(R.string.backup_complete_description),
onContinueClick = { },
),
)
}

View file

@ -4,16 +4,15 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
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.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.feature.FeatureBlock
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -91,39 +90,6 @@ internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modi
}
}
@Composable
private fun FeatureBlock(title: String, description: String, iconRes: Int, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
) {
Icon(
modifier = Modifier
.padding(horizontal = 12.dp),
painter = painterResource(iconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
) {
Text(
text = title,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier
.padding(top = 4.dp),
text = description,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable

View file

@ -27,7 +27,7 @@ internal class MobileWalletSetupFinishedComponent @AssistedInject constructor(
}
interface ModelCallbacks {
fun onContinueClick()
fun onFinishClick()
}
data class Params(

View file

@ -20,7 +20,7 @@ internal class MobileWalletSetupFinishedModel @Inject constructor(
internal val uiState: StateFlow<MobileWalletSetupFinishedUM>
field = MutableStateFlow(
MobileWalletSetupFinishedUM(
onContinueClick = params.callbacks::onContinueClick,
onFinishClick = params.callbacks::onFinishClick,
),
)
}

View file

@ -1,5 +1,5 @@
package com.tangem.features.hotwallet.setupfinished.entity
internal data class MobileWalletSetupFinishedUM(
val onContinueClick: () -> Unit,
val onFinishClick: () -> Unit,
)

View file

@ -70,7 +70,7 @@ internal fun MobileWalletSetupFinishedContent(state: MobileWalletSetupFinishedUM
top = 12.dp,
end = 48.dp,
),
text = stringResourceSafe(R.string.backup_complete_description),
text = stringResourceSafe(R.string.onboarding_done_wallet),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
@ -78,10 +78,10 @@ internal fun MobileWalletSetupFinishedContent(state: MobileWalletSetupFinishedUM
Spacer(modifier = Modifier.weight(2f))
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(R.string.common_continue),
text = stringResourceSafe(R.string.common_finish),
showProgress = false,
enabled = true,
onClick = state.onContinueClick,
onClick = state.onFinishClick,
)
}
@ -112,7 +112,7 @@ private fun PreviewMobileWalletSetupFinishedContent() {
TangemThemePreview {
MobileWalletSetupFinishedContent(
state = MobileWalletSetupFinishedUM(
onContinueClick = {},
onFinishClick = {},
),
)
}

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
@ -31,11 +32,19 @@ internal fun SetAccessCodeContent(
.imePadding()
.systemBarsPadding(),
) {
TangemTopAppBar(
modifier = Modifier,
title = stringResourceSafe(R.string.access_code_navtitle),
startButton = TopAppBarButtonUM.Back(onBackClick),
)
if (stackState.active.configuration is UpdateAccessCodeRoute.SetupFinished) {
TangemTopAppBar(
modifier = Modifier,
title = stringResourceSafe(R.string.access_code_navtitle),
titleAlignment = Alignment.CenterHorizontally,
)
} else {
TangemTopAppBar(
modifier = Modifier,
title = stringResourceSafe(R.string.access_code_navtitle),
startButton = TopAppBarButtonUM.Back(onBackClick),
)
}
Children(
stack = stackState,
animation = stackAnimation(slide()),

View file

@ -11,6 +11,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.hotwallet.UpdateAccessCodeComponent
import com.tangem.features.hotwallet.updateaccesscode.routing.UpdateAccessCodeRoute
import com.tangem.features.hotwallet.accesscode.AccessCodeComponent
import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
@ -27,11 +28,13 @@ internal class UpdateAccessCodeModel @Inject constructor(
val stackNavigation = StackNavigation<UpdateAccessCodeRoute>()
val startRoute: UpdateAccessCodeRoute = UpdateAccessCodeRoute.SetAccessCode(params.userWalletId)
val currentRoute: MutableStateFlow<UpdateAccessCodeRoute> = MutableStateFlow(startRoute)
val mobileWalletSetupFinishedComponentModelCallbacks = MobileWalletSetupFinishedComponentModelCallbacks()
fun onChildBack() {
when (currentRoute.value) {
is UpdateAccessCodeRoute.SetAccessCode -> router.pop()
is UpdateAccessCodeRoute.ConfirmAccessCode -> stackNavigation.pop()
is UpdateAccessCodeRoute.SetupFinished -> Unit
}
}
@ -40,6 +43,16 @@ internal class UpdateAccessCodeModel @Inject constructor(
}
override fun onAccessCodeUpdated(userWalletId: UserWalletId) {
router.pop()
if (params.isFirstSetup) {
stackNavigation.push(UpdateAccessCodeRoute.SetupFinished)
} else {
router.pop()
}
}
inner class MobileWalletSetupFinishedComponentModelCallbacks : MobileWalletSetupFinishedComponent.ModelCallbacks {
override fun onFinishClick() {
router.pop()
}
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.updateaccesscode.UpdateAccessCodeModel
import com.tangem.features.hotwallet.accesscode.AccessCodeComponent
import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent
import javax.inject.Inject
internal class UpdateAccessCodeChildFactory @Inject constructor(
@ -31,6 +32,12 @@ internal class UpdateAccessCodeChildFactory @Inject constructor(
callbacks = model,
),
)
is UpdateAccessCodeRoute.SetupFinished -> MobileWalletSetupFinishedComponent(
context = childContext,
params = MobileWalletSetupFinishedComponent.Params(
callbacks = model.mobileWalletSetupFinishedComponentModelCallbacks,
),
)
}
}
}

View file

@ -16,4 +16,7 @@ internal sealed class UpdateAccessCodeRoute : Route {
val userWalletId: UserWalletId,
val accessCode: String,
) : UpdateAccessCodeRoute()
@Serializable
data object SetupFinished : UpdateAccessCodeRoute()
}

View file

@ -5,21 +5,39 @@ 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.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.hotwallet.UpgradeWalletComponent
import com.tangem.features.hotwallet.upgradewallet.ui.UpgradeWalletContent
import com.tangem.features.onboarding.v2.util.ResetCardsComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@Suppress("UnusedPrivateMember")
internal class DefaultUpgradeWalletComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: UpgradeWalletComponent.Params,
resetCardsComponentFactory: ResetCardsComponent.Factory,
) : UpgradeWalletComponent, AppComponentContext by context {
private val model: UpgradeWalletModel = getOrCreateModel(params)
private val resetCardsComponent = resetCardsComponentFactory.create(
context = child("ResetCardsComponent"),
params = ResetCardsComponent.Params(
callbacks = model.resetCardsComponentCallbacks,
),
)
init {
model.startResetCardsFlow
.onEach { resetCardsComponent.startResetCardsFlow(it) }
.launchIn(componentScope)
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()

View file

@ -3,6 +3,7 @@ package com.tangem.features.hotwallet.upgradewallet
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped
@ -16,20 +17,25 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.card.BackupValidator
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.usecase.ClearHotWalletContextualUnlockUseCase
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
import com.tangem.features.hotwallet.UpgradeWalletComponent
import com.tangem.features.hotwallet.upgradewallet.entity.UpgradeWalletUM
import com.tangem.features.onboarding.v2.util.ResetCardsComponent
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
@ -50,17 +56,21 @@ internal class UpgradeWalletModel @Inject constructor(
private val clearHotWalletContextualUnlockUseCase: ClearHotWalletContextualUnlockUseCase,
private val tangemSdkManager: TangemSdkManager,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
) : 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
val resetCardsComponentCallbacks = ResetCardsModelCallbacks()
val startResetCardsFlow = MutableSharedFlow<UserWallet.Cold>()
internal val uiState: StateFlow<UpgradeWalletUM>
field = MutableStateFlow(
UpgradeWalletUM(
onBackClick = { router.pop() },
onBuyTangemWalletClick = ::onBuyTangemWalletClick,
onContinueClick = ::onContinueClick,
),
)
override fun onDestroy() {
clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
@ -73,7 +83,7 @@ internal class UpgradeWalletModel @Inject constructor(
}
}
private fun onScanDeviceClick() {
private fun onContinueClick() {
scanCard()
}
@ -89,6 +99,22 @@ internal class UpgradeWalletModel @Inject constructor(
tangemSdkManager
.scanProduct()
.doOnSuccess {
// Check if user attempted to upgrade before but something went wrong and a full reset is required
val userWallet = coldUserWalletBuilderFactory.create(it).build()
val sameWalletButNotFinishedBackup by lazy {
userWallet?.walletId == params.userWalletId &&
BackupValidator.isValidFull(it.card).not()
}
val otherWalletAndAlreadyCreated by lazy {
userWallet?.walletId != params.userWalletId &&
it.card.wallets.map { it.curve }.toSet().isNotEmpty()
}
if (userWallet != null && (sameWalletButNotFinishedBackup || otherWalletAndAlreadyCreated)) {
startResetCardsFlow.emit(userWallet)
return@doOnSuccess
}
delay(DELAY_SDK_DIALOG_CLOSE)
tangemSdkManager.changeDisplayedCardIdNumbersCount(it)
navigateToUpgradeFlow(it)
@ -96,11 +122,14 @@ internal class UpgradeWalletModel @Inject constructor(
.doOnFailure {
showCardVerificationFailedDialog(it)
}
.doOnResult {
setLoading(false)
}
}
}
private fun setLoading(isLoading: Boolean) {
_uiState.update { it.copy(isLoading = isLoading) }
uiState.update { it.copy(isLoading = isLoading) }
}
private fun showCardVerificationFailedDialog(error: TangemError) {
@ -143,4 +172,14 @@ internal class UpgradeWalletModel @Inject constructor(
),
)
}
inner class ResetCardsModelCallbacks : ResetCardsComponent.ModelCallbacks {
override fun onCancel() {
setLoading(false)
}
override fun onComplete() {
setLoading(false)
}
}
}

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
@ -16,6 +18,7 @@ import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.feature.FeatureBlock
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -102,44 +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,
)
}
}
}
@Composable
private fun FeatureBlock(title: String, description: String, iconRes: Int, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
) {
Icon(
modifier = Modifier
.padding(horizontal = 12.dp),
painter = painterResource(iconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
) {
Text(
text = title,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier
.padding(top = 4.dp),
text = description,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
text = stringResourceSafe(R.string.hw_upgrade_start_action),
onClick = state.onContinueClick,
showProgress = state.isLoading,
)
}
}
@ -148,13 +118,28 @@ private fun FeatureBlock(title: String, description: String, iconRes: Int, modif
@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

@ -6,6 +6,7 @@ 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.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.features.hotwallet.ViewPhraseComponent
import com.tangem.features.hotwallet.viewphrase.model.ViewPhraseModel
import com.tangem.features.hotwallet.viewphrase.ui.ViewPhraseContent
@ -22,6 +23,9 @@ internal class DefaultViewPhraseComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
DisableScreenshotsDisposableEffect()
ViewPhraseContent(
state = state,
modifier = modifier,

View file

@ -4,6 +4,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.push
import com.arkivanov.decompose.router.stack.replaceAll
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
@ -16,6 +17,7 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase
import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent
@ -32,6 +34,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class WalletActivationModel @Inject constructor(
paramsContainer: ParamsContainer,
@ -39,6 +42,8 @@ internal class WalletActivationModel @Inject constructor(
private val router: Router,
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val trackingContextProxy: TrackingContextProxy,
private val setAccessCodeSkippedUseCase: SetAccessCodeSkippedUseCase,
) : Model() {
val params = paramsContainer.require<WalletActivationComponent.Params>()
@ -52,17 +57,33 @@ internal class WalletActivationModel @Inject constructor(
val pushNotificationsCallbacks = PushNotificationsCallbacks()
val mobileWalletSetupFinishedModelCallbacks = MobileWalletSetupFinishedModelCallbacks()
val isStartingWithAccessCode = params.isBackupExists
val stackNavigation = StackNavigation<WalletActivationRoute>()
val startRoute = WalletActivationRoute.ManualBackupStart
val startRoute = if (isStartingWithAccessCode) {
WalletActivationRoute.SetAccessCode
} else {
WalletActivationRoute.ManualBackupStart
}
val currentRoute: MutableStateFlow<WalletActivationRoute> = MutableStateFlow(startRoute)
init {
trackingContextProxy.addHotWalletContext()
}
override fun onDestroy() {
super.onDestroy()
trackingContextProxy.removeContext()
}
fun onChildBack() {
when (currentRoute.value) {
is WalletActivationRoute.ManualBackupStart -> router.pop()
is WalletActivationRoute.ManualBackupPhrase -> stackNavigation.pop()
is WalletActivationRoute.ManualBackupCheck -> stackNavigation.pop()
is WalletActivationRoute.ManualBackupCompleted -> Unit
is WalletActivationRoute.SetAccessCode -> Unit
is WalletActivationRoute.SetAccessCode -> if (isStartingWithAccessCode) {
router.pop()
}
is WalletActivationRoute.ConfirmAccessCode -> stackNavigation.pop()
is WalletActivationRoute.PushNotifications -> Unit
is WalletActivationRoute.SetupFinished -> Unit
@ -85,6 +106,8 @@ internal class WalletActivationModel @Inject constructor(
}
private fun showSkipAccessCodeWarningDialog() {
val userWalletId = params.userWalletId
uiMessageSender.send(
DialogMessage(
message = resourceReference(R.string.access_code_alert_skip_description),
@ -95,7 +118,12 @@ internal class WalletActivationModel @Inject constructor(
),
secondAction = EventMessageAction(
title = resourceReference(R.string.access_code_alert_skip_ok),
onClick = { navigateToPushNotificationsOrNext() },
onClick = {
modelScope.launch {
setAccessCodeSkippedUseCase(userWalletId, true)
}
navigateToPushNotificationsOrNext()
},
),
shouldDismissOnFirstAction = true,
),
@ -136,6 +164,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 {
@ -163,7 +193,7 @@ internal class WalletActivationModel @Inject constructor(
}
inner class MobileWalletSetupFinishedModelCallbacks : MobileWalletSetupFinishedComponent.ModelCallbacks {
override fun onContinueClick() {
override fun onFinishClick() {
router.pop()
}
}

View file

@ -55,7 +55,7 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
steps = STEPS_COUNT,
title = resourceReference(R.string.access_code_navtitle),
showBackButton = true,
showSkipButton = false,
showSkipButton = true,
showFeedbackButton = false,
)
is WalletActivationRoute.PushNotifications -> HotWalletStepperComponent.StepperUM(

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,11 @@ internal class WalletBackupModel @Inject constructor(
},
)
} else {
router.push(AppRoute.CreateWalletBackup(params.userWalletId))
router.push(
AppRoute.CreateWalletBackup(
userWalletId = params.userWalletId,
),
)
}
}
@ -132,6 +148,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,154 @@
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.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.domain.wallets.usecase.UnlockHotWalletContextualUseCase
import com.tangem.features.hotwallet.WalletHardwareBackupComponent
import com.tangem.features.hotwallet.impl.R
import com.tangem.features.hotwallet.wallethardwarebackup.entity.WalletHardwareBackupUM
import com.tangem.hot.sdk.model.HotWalletId
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 timber.log.Timber
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 unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase,
private val urlOpener: UrlOpener,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val messageSender: UiMessageSender,
) : Model() {
private val params = paramsContainer.require<WalletHardwareBackupComponent.Params>()
private val makeBackupAtFirstAlertBS
get() = bottomSheetMessage {
infoBlock {
icon(R.drawable.ic_passcode_lock_32) {
type = MessageBottomSheetUMV2.Icon.Type.Accent
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
}
title = resourceReference(R.string.hw_backup_need_finish_first)
body = resourceReference(R.string.hw_backup_to_upgrade_description)
}
primaryButton {
text = resourceReference(R.string.hw_backup_need_action)
onClick {
router.push(
AppRoute.CreateWalletBackup(
userWalletId = params.userWalletId,
isUpgradeFlow = true,
),
)
closeBs()
}
}
}
internal val uiState: StateFlow<WalletHardwareBackupUM>
field = MutableStateFlow(
WalletHardwareBackupUM(
onBackClick = { router.pop() },
blocks = persistentListOf(
WalletHardwareBackupUM.Block(
title = resourceReference(R.string.hw_backup_hardware_create_title),
titleLabel = LabelUM(
text = resourceReference(R.string.common_recommended),
style = LabelStyle.ACCENT,
),
description = resourceReference(R.string.hw_backup_hardware_create_description),
onClick = ::onCreateNewWalletClick,
),
WalletHardwareBackupUM.Block(
title = resourceReference(R.string.hw_backup_hardware_upgrade_title),
titleLabel = null,
description = resourceReference(R.string.hw_backup_hardware_upgrade_description),
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 {
val hotWalletId = userWallet.hotWalletId
when (hotWalletId.authType) {
HotWalletId.AuthType.NoPassword -> {
router.push(AppRoute.UpgradeWallet(userWalletId = params.userWalletId))
}
HotWalletId.AuthType.Password,
HotWalletId.AuthType.Biometry,
-> modelScope.launch {
unlockHotWalletContextualUseCase.invoke(hotWalletId)
.onLeft {
Timber.e(it, "Unable to unlock wallet with id ${params.userWalletId}")
}
.onRight {
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 = { },
),
)
}
}

Some files were not shown because too many files have changed in this diff Show more