Updated on 2026-08-14
This commit is contained in:
commit
9aacdb2ed3
695 changed files with 22022 additions and 5116 deletions
|
|
@ -22,7 +22,12 @@ interface PortfolioFetcher {
|
|||
val appCurrency: AppCurrency,
|
||||
val isBalanceHidden: Boolean,
|
||||
val balances: Map<UserWallet, PortfolioBalance>,
|
||||
)
|
||||
) {
|
||||
|
||||
val isSingleChoice: Boolean = balances.values
|
||||
.map { it.accountsBalance.accountStatuses }
|
||||
.flatten().size == 1
|
||||
}
|
||||
|
||||
data class PortfolioBalance(
|
||||
val userWallet: UserWallet,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,5 +24,6 @@ internal data class ArchivedAccountUM(
|
|||
val accountIconUM: CryptoPortfolioIconUM,
|
||||
val tokensInfo: TextReference,
|
||||
val networksInfo: TextReference,
|
||||
val isLoading: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ internal data class AccountDetailsUM(
|
|||
data object None : ArchiveMode
|
||||
data class Available(
|
||||
val onArchiveAccountClick: () -> Unit,
|
||||
val isLoading: Boolean,
|
||||
) : ArchiveMode
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
},
|
||||
|
|
|
|||
|
|
@ -41,7 +41,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)
|
||||
|
|
|
|||
|
|
@ -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,15 +73,17 @@ 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
|
||||
|
|
@ -94,12 +101,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,6 +125,7 @@ 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
|
||||
|
|
@ -133,15 +143,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 +172,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 +186,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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ sealed interface PortfolioSelectorItemUM {
|
|||
|
||||
data class Portfolio(
|
||||
val item: UserWalletItemUM,
|
||||
val isSelected: Boolean,
|
||||
) : PortfolioSelectorItemUM {
|
||||
override val id: String = item.id.stringValue
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -68,14 +71,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) },
|
||||
|
|
@ -158,16 +168,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 +187,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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -135,13 +135,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")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ internal class CreateWalletSelectionModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onHardwareWalletClick() {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
router.push(AppRoute.CreateHardwareWallet)
|
||||
}
|
||||
|
||||
private fun onBuyClick() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ interface CreateWalletBackupComponent : ComposableContentComponent {
|
|||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val isUpgradeFlow: Boolean,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, CreateWalletBackupComponent>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ android {
|
|||
dependencies {
|
||||
/** Api */
|
||||
implementation(projects.features.hotWallet.api)
|
||||
implementation(projects.features.onboardingV2.api)
|
||||
implementation(projects.features.pushNotifications.api)
|
||||
|
||||
/** Core modules */
|
||||
|
|
|
|||
|
|
@ -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.AnalyticsContextProxy
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -32,6 +33,7 @@ internal class AddExistingWalletModel @Inject constructor(
|
|||
private val router: Router,
|
||||
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsContextProxy: AnalyticsContextProxy,
|
||||
) : Model() {
|
||||
|
||||
val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback()
|
||||
|
|
@ -45,6 +47,15 @@ internal class AddExistingWalletModel @Inject constructor(
|
|||
val startRoute = AddExistingWalletRoute.Import
|
||||
val currentRoute: MutableStateFlow<AddExistingWalletRoute> = MutableStateFlow(startRoute)
|
||||
|
||||
init {
|
||||
analyticsContextProxy.addHotWalletContext()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
analyticsContextProxy.removeContext()
|
||||
}
|
||||
|
||||
fun onChildBack() {
|
||||
when (currentRoute.value) {
|
||||
is AddExistingWalletRoute.Import -> router.pop()
|
||||
|
|
@ -109,6 +120,8 @@ internal class AddExistingWalletModel @Inject constructor(
|
|||
override fun onContinueClick(userWalletId: UserWalletId) {
|
||||
stackNavigation.replaceAll(AddExistingWalletRoute.SetAccessCode(userWalletId))
|
||||
}
|
||||
|
||||
override fun onUpgradeClick(userWalletId: UserWalletId) = Unit
|
||||
}
|
||||
|
||||
inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ internal class AddExistingWalletChildFactory @Inject constructor(
|
|||
params = ManualBackupCompletedComponent.Params(
|
||||
userWalletId = route.userWalletId,
|
||||
callbacks = model.manualBackupCompletedComponentModelCallbacks,
|
||||
isUpgradeFlow = false,
|
||||
),
|
||||
)
|
||||
is AddExistingWalletRoute.SetAccessCode -> accessCodeComponentFactory.create(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.hotwallet.createmobilewallet
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.utils.AnalyticsContextProxy
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.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 analyticsContextProxy: AnalyticsContextProxy,
|
||||
) : Model() {
|
||||
|
||||
internal val uiState: StateFlow<CreateMobileWalletUM>
|
||||
|
|
@ -37,6 +39,15 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
init {
|
||||
analyticsContextProxy.addHotWalletContext()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
analyticsContextProxy.removeContext()
|
||||
}
|
||||
|
||||
private fun onImportClick() {
|
||||
router.push(AppRoute.AddExistingWallet)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.AnalyticsContextProxy
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.navigation.popTo
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.hotwallet.CreateWalletBackupComponent
|
||||
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute
|
||||
|
|
@ -23,6 +26,7 @@ internal class CreateWalletBackupModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val analyticsContextProxy: AnalyticsContextProxy,
|
||||
) : 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 {
|
||||
analyticsContextProxy.addHotWalletContext()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
analyticsContextProxy.removeContext()
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
when (currentRoute.value) {
|
||||
is CreateWalletBackupRoute.RecoveryPhraseStart -> router.pop()
|
||||
|
|
@ -54,7 +67,7 @@ internal class CreateWalletBackupModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onManualBackupChecked() {
|
||||
stackNavigation.push(CreateWalletBackupRoute.BackupCompleted)
|
||||
stackNavigation.push(CreateWalletBackupRoute.BackupCompleted(isUpgradeFlow = params.isUpgradeFlow))
|
||||
}
|
||||
|
||||
fun onManualBackupCompleted() {
|
||||
|
|
@ -83,5 +96,14 @@ internal class CreateWalletBackupModel @Inject constructor(
|
|||
override fun onContinueClick(userWalletId: UserWalletId) {
|
||||
onManualBackupCompleted()
|
||||
}
|
||||
|
||||
override fun onUpgradeClick(userWalletId: UserWalletId) {
|
||||
router.popTo<AppRoute.WalletSettings>()
|
||||
router.push(
|
||||
AppRoute.UpgradeWallet(
|
||||
userWalletId = userWalletId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,5 +15,7 @@ internal sealed interface CreateWalletBackupRoute {
|
|||
data object ConfirmBackup : CreateWalletBackupRoute
|
||||
|
||||
@Serializable
|
||||
data object BackupCompleted : CreateWalletBackupRoute
|
||||
data class BackupCompleted(
|
||||
val isUpgradeFlow: Boolean,
|
||||
) : CreateWalletBackupRoute
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package com.tangem.features.hotwallet.forgetwallet
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
|
||||
import com.tangem.features.hotwallet.ForgetWalletComponent
|
||||
import com.tangem.features.hotwallet.forgetwallet.entity.ForgetWalletUM
|
||||
import com.tangem.features.hotwallet.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class ForgetWalletModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val deleteWalletUseCase: DeleteWalletUseCase,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ForgetWalletComponent.Params>()
|
||||
|
||||
internal val uiState: StateFlow<ForgetWalletUM>
|
||||
field = MutableStateFlow(
|
||||
ForgetWalletUM(
|
||||
onBackClick = { router.pop() },
|
||||
firstCheckboxChecked = false,
|
||||
secondCheckboxChecked = false,
|
||||
onFirstCheckboxClick = ::onFirstCheckboxClick,
|
||||
onSecondCheckboxClick = ::onSecondCheckboxClick,
|
||||
onForgetWalletClick = ::onForgetWalletClick,
|
||||
isForgetButtonEnabled = false,
|
||||
),
|
||||
)
|
||||
|
||||
private fun onFirstCheckboxClick() {
|
||||
uiState.update {
|
||||
val newValue = !it.firstCheckboxChecked
|
||||
it.copy(
|
||||
firstCheckboxChecked = newValue,
|
||||
isForgetButtonEnabled = newValue && it.secondCheckboxChecked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSecondCheckboxClick() {
|
||||
uiState.update {
|
||||
val newValue = !it.secondCheckboxChecked
|
||||
it.copy(
|
||||
secondCheckboxChecked = newValue,
|
||||
isForgetButtonEnabled = it.firstCheckboxChecked && newValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onForgetWalletClick() {
|
||||
// TODO actualize strings [REDACTED_TASK_KEY]
|
||||
uiMessageSender.send(
|
||||
DialogMessage(
|
||||
title = stringReference("Attention"),
|
||||
message = stringReference("Are you sure you want to do this?"),
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = stringReference("Forget"),
|
||||
isWarning = true,
|
||||
onClick = ::forgetWallet,
|
||||
)
|
||||
},
|
||||
secondActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = stringReference("Cancel"),
|
||||
onClick = {},
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun forgetWallet() {
|
||||
modelScope.launch {
|
||||
val hasUserWallets = deleteWalletUseCase(params.userWalletId)
|
||||
.getOrElse {
|
||||
Timber.e("Unable to delete wallet: $it")
|
||||
|
||||
uiMessageSender.send(
|
||||
message = SnackbarMessage(resourceReference(R.string.common_unknown_error)),
|
||||
)
|
||||
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (hasUserWallets) {
|
||||
router.pop()
|
||||
} else {
|
||||
router.replaceAll(AppRoute.Home())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.tangem.features.hotwallet.forgetwallet.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.hotwallet.forgetwallet.entity.ForgetWalletUM
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun ForgetWalletContent(state: ForgetWalletUM, modifier: Modifier = Modifier) {
|
||||
// TODO actualize strings [REDACTED_TASK_KEY]
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding(),
|
||||
) {
|
||||
TangemTopAppBar(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
startButton = TopAppBarButtonUM.Back(state.onBackClick),
|
||||
title = TextReference.EMPTY,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_attention_72),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.warning,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = "Attention",
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = "This wallet will be permanently removed from your device",
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
CheckboxItem(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
checked = state.firstCheckboxChecked,
|
||||
onCheckedChange = state.onFirstCheckboxClick,
|
||||
text = "I understand that removing my wallet does not delete it—but simply removes it from my device.",
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
CheckboxItem(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
checked = state.secondCheckboxChecked,
|
||||
onCheckedChange = state.onSecondCheckboxClick,
|
||||
text = "I understand that if I haven't backed up my wallet before removing it, I may lose access to it.",
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
text = "Forget wallet",
|
||||
onClick = state.onForgetWalletClick,
|
||||
enabled = state.isForgetButtonEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CheckboxItem(checked: Boolean, onCheckedChange: () -> Unit, text: String, modifier: Modifier = Modifier) {
|
||||
// TODO actualize strings [REDACTED_TASK_KEY]
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
IconToggleButton(
|
||||
checked = checked,
|
||||
onCheckedChange = { onCheckedChange() },
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = checked,
|
||||
label = "Update checked state",
|
||||
) { isChecked ->
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
if (isChecked) {
|
||||
R.drawable.ic_accepted_20
|
||||
} else {
|
||||
R.drawable.ic_unticked_20
|
||||
},
|
||||
),
|
||||
contentDescription = null,
|
||||
tint = if (isChecked) {
|
||||
TangemTheme.colors.control.checked
|
||||
} else {
|
||||
TangemTheme.colors.icon.secondary
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = text,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewForgetWalletContent() {
|
||||
TangemThemePreview {
|
||||
ForgetWalletContent(
|
||||
state = ForgetWalletUM(
|
||||
onBackClick = {},
|
||||
firstCheckboxChecked = true,
|
||||
secondCheckboxChecked = false,
|
||||
onFirstCheckboxClick = {},
|
||||
onSecondCheckboxClick = {},
|
||||
onForgetWalletClick = {},
|
||||
isForgetButtonEnabled = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -29,10 +29,12 @@ internal class ManualBackupCompletedComponent @AssistedInject constructor(
|
|||
|
||||
interface ModelCallbacks {
|
||||
fun onContinueClick(userWalletId: UserWalletId)
|
||||
fun onUpgradeClick(userWalletId: UserWalletId)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val callbacks: ModelCallbacks,
|
||||
val isUpgradeFlow: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class ManualBackupCompletedModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
|
|
@ -20,7 +21,14 @@ internal class ManualBackupCompletedModel @Inject constructor(
|
|||
internal val uiState: StateFlow<ManualBackupCompletedUM>
|
||||
field = MutableStateFlow(
|
||||
ManualBackupCompletedUM(
|
||||
onContinueClick = { params.callbacks.onContinueClick(params.userWalletId) },
|
||||
onContinueClick = {
|
||||
if (params.isUpgradeFlow) {
|
||||
params.callbacks.onUpgradeClick(params.userWalletId)
|
||||
} else {
|
||||
params.callbacks.onContinueClick(params.userWalletId)
|
||||
}
|
||||
},
|
||||
isLoading = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -2,4 +2,5 @@ package com.tangem.features.hotwallet.manualbackup.completed.entity
|
|||
|
||||
internal data class ManualBackupCompletedUM(
|
||||
val onContinueClick: () -> Unit,
|
||||
val isLoading: Boolean,
|
||||
)
|
||||
|
|
@ -65,8 +65,6 @@ internal fun ManualBackupCompletedContent(state: ManualBackupCompletedUM, modifi
|
|||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.common_continue),
|
||||
showProgress = false,
|
||||
enabled = true,
|
||||
onClick = state.onContinueClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -75,11 +73,26 @@ internal fun ManualBackupCompletedContent(state: ManualBackupCompletedUM, modifi
|
|||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewManualBackupCompletedContent() {
|
||||
private fun PreviewManualBackupCompletedContentRegular() {
|
||||
TangemThemePreview {
|
||||
ManualBackupCompletedContent(
|
||||
state = ManualBackupCompletedUM(
|
||||
onContinueClick = {},
|
||||
isLoading = false,
|
||||
onContinueClick = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewManualBackupCompletedContentUpgrade() {
|
||||
TangemThemePreview {
|
||||
ManualBackupCompletedContent(
|
||||
state = ManualBackupCompletedUM(
|
||||
isLoading = false,
|
||||
onContinueClick = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -16,20 +16,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 +55,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 +82,7 @@ internal class UpgradeWalletModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onScanDeviceClick() {
|
||||
private fun onContinueClick() {
|
||||
scanCard()
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +98,13 @@ 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()
|
||||
if (userWallet?.walletId == params.userWalletId && BackupValidator.isValidFull(it.card).not()) {
|
||||
startResetCardsFlow.emit(userWallet)
|
||||
return@doOnSuccess
|
||||
}
|
||||
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
tangemSdkManager.changeDisplayedCardIdNumbersCount(it)
|
||||
navigateToUpgradeFlow(it)
|
||||
|
|
@ -100,7 +116,7 @@ internal class UpgradeWalletModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun setLoading(isLoading: Boolean) {
|
||||
_uiState.update { it.copy(isLoading = isLoading) }
|
||||
uiState.update { it.copy(isLoading = isLoading) }
|
||||
}
|
||||
|
||||
private fun showCardVerificationFailedDialog(error: TangemError) {
|
||||
|
|
@ -143,4 +159,14 @@ internal class UpgradeWalletModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
inner class ResetCardsModelCallbacks : ResetCardsComponent.ModelCallbacks {
|
||||
override fun onCancel() {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.AnalyticsContextProxy
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -39,6 +40,7 @@ internal class WalletActivationModel @Inject constructor(
|
|||
private val router: Router,
|
||||
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsContextProxy: AnalyticsContextProxy,
|
||||
) : Model() {
|
||||
|
||||
val params = paramsContainer.require<WalletActivationComponent.Params>()
|
||||
|
|
@ -56,6 +58,15 @@ internal class WalletActivationModel @Inject constructor(
|
|||
val startRoute = WalletActivationRoute.ManualBackupStart
|
||||
val currentRoute: MutableStateFlow<WalletActivationRoute> = MutableStateFlow(startRoute)
|
||||
|
||||
init {
|
||||
analyticsContextProxy.addHotWalletContext()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
analyticsContextProxy.removeContext()
|
||||
}
|
||||
|
||||
fun onChildBack() {
|
||||
when (currentRoute.value) {
|
||||
is WalletActivationRoute.ManualBackupStart -> router.pop()
|
||||
|
|
@ -136,6 +147,8 @@ internal class WalletActivationModel @Inject constructor(
|
|||
override fun onContinueClick(userWalletId: UserWalletId) {
|
||||
stackNavigation.push(WalletActivationRoute.SetAccessCode)
|
||||
}
|
||||
|
||||
override fun onUpgradeClick(userWalletId: UserWalletId) = Unit
|
||||
}
|
||||
|
||||
inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ internal class WalletActivationChildFactory @Inject constructor(
|
|||
params = ManualBackupCompletedComponent.Params(
|
||||
userWalletId = model.params.userWalletId,
|
||||
callbacks = model.manualBackupCompletedModelCallbacks,
|
||||
isUpgradeFlow = false,
|
||||
),
|
||||
)
|
||||
is WalletActivationRoute.SetAccessCode -> accessCodeComponentFactory.create(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
package com.tangem.features.hotwallet.walletbackup.model
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.label.entity.LabelStyle
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.domain.wallets.usecase.UnlockHotWalletContextualUseCase
|
||||
import com.tangem.features.hotwallet.WalletBackupComponent
|
||||
import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus
|
||||
|
|
@ -21,12 +23,15 @@ import kotlinx.coroutines.launch
|
|||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class WalletBackupModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val router: Router,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -45,6 +50,7 @@ internal class WalletBackupModel @Inject constructor(
|
|||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
googleDriveStatus = BackupStatus.ComingSoon,
|
||||
onBuyClick = ::onBuyClick,
|
||||
onRecoveryPhraseClick = ::onRecoveryPhraseClick,
|
||||
onGoogleDriveClick = { },
|
||||
onHardwareWalletClick = ::onHardwareWalletClick,
|
||||
|
|
@ -95,6 +101,12 @@ internal class WalletBackupModel @Inject constructor(
|
|||
backedUp = userWallet.backedUp,
|
||||
)
|
||||
|
||||
private fun onBuyClick() {
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRecoveryPhraseClick() {
|
||||
if (uiState.value.backedUp) {
|
||||
getUserWalletUseCase.invoke(params.userWalletId)
|
||||
|
|
@ -113,7 +125,12 @@ internal class WalletBackupModel @Inject constructor(
|
|||
},
|
||||
)
|
||||
} else {
|
||||
router.push(AppRoute.CreateWalletBackup(params.userWalletId))
|
||||
router.push(
|
||||
AppRoute.CreateWalletBackup(
|
||||
userWalletId = params.userWalletId,
|
||||
isUpgradeFlow = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +149,6 @@ internal class WalletBackupModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onHardwareWalletClick() {
|
||||
router.push(AppRoute.UpgradeWallet(params.userWalletId))
|
||||
router.push(AppRoute.WalletHardwareBackup(params.userWalletId))
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
package com.tangem.features.hotwallet.wallethardwarebackup.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
|
||||
import com.tangem.core.ui.components.bottomsheets.message.icon
|
||||
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
|
||||
import com.tangem.core.ui.components.bottomsheets.message.onClick
|
||||
import com.tangem.core.ui.components.bottomsheets.message.primaryButton
|
||||
import com.tangem.core.ui.components.label.entity.LabelStyle
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.bottomSheetMessage
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.hotwallet.WalletHardwareBackupComponent
|
||||
import com.tangem.features.hotwallet.impl.R
|
||||
import com.tangem.features.hotwallet.wallethardwarebackup.entity.WalletHardwareBackupUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class WalletHardwareBackupModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<WalletHardwareBackupComponent.Params>()
|
||||
|
||||
// TODO actualize strings [REDACTED_TASK_KEY]
|
||||
private val makeBackupAtFirstAlertBS
|
||||
get() = bottomSheetMessage {
|
||||
infoBlock {
|
||||
icon(R.drawable.ic_passcode_lock_32) {
|
||||
type = MessageBottomSheetUMV2.Icon.Type.Accent
|
||||
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
|
||||
}
|
||||
title = stringReference("Finish Backup First")
|
||||
body = stringReference("To upgrade your wallet to hardware, back it up first.")
|
||||
}
|
||||
primaryButton {
|
||||
text = resourceReference(R.string.hw_backup_need_action)
|
||||
onClick {
|
||||
router.push(
|
||||
AppRoute.CreateWalletBackup(
|
||||
userWalletId = params.userWalletId,
|
||||
isUpgradeFlow = true,
|
||||
),
|
||||
)
|
||||
closeBs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO actualize strings [REDACTED_TASK_KEY]
|
||||
internal val uiState: StateFlow<WalletHardwareBackupUM>
|
||||
field = MutableStateFlow(
|
||||
WalletHardwareBackupUM(
|
||||
onBackClick = { router.pop() },
|
||||
blocks = persistentListOf(
|
||||
WalletHardwareBackupUM.Block(
|
||||
title = stringReference("Create new wallet"),
|
||||
titleLabel = LabelUM(
|
||||
text = resourceReference(R.string.common_recommended),
|
||||
style = LabelStyle.ACCENT,
|
||||
),
|
||||
description = stringReference(
|
||||
"Create a new secure wallet and transfer your funds for extra protection.",
|
||||
),
|
||||
onClick = ::onCreateNewWalletClick,
|
||||
),
|
||||
WalletHardwareBackupUM.Block(
|
||||
title = stringReference("Upgrade current wallet"),
|
||||
titleLabel = null,
|
||||
description = stringReference("Move your current wallet into Tangem Wallet."),
|
||||
onClick = ::onUpgradeCurrentWalletClick,
|
||||
),
|
||||
),
|
||||
onBuyClick = ::onBuyClick,
|
||||
),
|
||||
)
|
||||
|
||||
init {
|
||||
showPurchaseBlockWithDelay()
|
||||
}
|
||||
|
||||
private fun showPurchaseBlockWithDelay() {
|
||||
modelScope.launch {
|
||||
delay(SHOW_PURCHASE_BLOCK_DELAY)
|
||||
uiState.update { it.copy(showPurchaseBlock = true) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCreateNewWalletClick() {
|
||||
router.push(AppRoute.CreateHardwareWallet)
|
||||
}
|
||||
|
||||
private fun onUpgradeCurrentWalletClick() {
|
||||
val userWallet = getUserWalletUseCase.invoke(params.userWalletId)
|
||||
.getOrElse { error("Cannot find user wallet with id: ${params.userWalletId.stringValue}") }
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
if (!userWallet.backedUp) {
|
||||
messageSender.send(makeBackupAtFirstAlertBS)
|
||||
} else {
|
||||
router.push(
|
||||
AppRoute.UpgradeWallet(
|
||||
userWalletId = params.userWalletId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBuyClick() {
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SHOW_PURCHASE_BLOCK_DELAY = 3000L
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,9 +9,13 @@ import com.tangem.core.decompose.ui.UiMessageSender
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase
|
||||
import com.tangem.domain.managetokens.FindTokenUseCase
|
||||
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
|
||||
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
|
||||
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
|
||||
import com.tangem.features.managetokens.component.CustomTokenFormComponent
|
||||
import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM
|
||||
|
|
@ -41,6 +45,10 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
private val messageSender: UiMessageSender,
|
||||
private val customTokenFormManager: CustomCurrencyFormBuilder,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
|
||||
private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase,
|
||||
private val findTokenUseCase: FindTokenUseCase,
|
||||
private val validateTokenFormUseCase: ValidateTokenFormUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
customTokenFormUseCasesFacadeFactory: CustomTokenFormUseCasesFacade.Factory,
|
||||
) : Model() {
|
||||
|
|
@ -48,7 +56,13 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
private val params: CustomTokenFormComponent.Params = paramsContainer.require()
|
||||
private var createdCurrency: CryptoCurrency? = null
|
||||
private var useCasesFacade: CustomTokenFormUseCasesFacade = customTokenFormUseCasesFacadeFactory.create(params.mode)
|
||||
private val customCurrencyValidator = CustomCurrencyValidator(useCasesFacade)
|
||||
private val customCurrencyValidator = CustomCurrencyValidator(
|
||||
userWalletId = params.mode.userWalletId,
|
||||
useCasesFacade = useCasesFacade,
|
||||
createCryptoCurrencyUseCase = createCryptoCurrencyUseCase,
|
||||
findTokenUseCase = findTokenUseCase,
|
||||
validateTokenFormUseCase = validateTokenFormUseCase,
|
||||
)
|
||||
|
||||
val state: MutableStateFlow<CustomTokenFormUM> = MutableStateFlow(
|
||||
value = getInitialState(),
|
||||
|
|
@ -164,8 +178,9 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
isAlreadyAdded: Boolean,
|
||||
isCustom: Boolean,
|
||||
) = modelScope.launch {
|
||||
val needColdWalletInteraction = useCasesFacade.needColdWalletInteraction(
|
||||
network = mapOf(currency.network.backendId to getDerivationPath().value),
|
||||
val needColdWalletInteraction = coldWalletAndHasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = params.mode.userWalletId,
|
||||
networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value),
|
||||
)
|
||||
|
||||
state.update { state ->
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
package com.tangem.features.managetokens.utils
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase
|
||||
import com.tangem.domain.managetokens.FindTokenUseCase
|
||||
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
|
||||
import com.tangem.domain.managetokens.model.AddCustomTokenForm
|
||||
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
|
||||
import com.tangem.domain.managetokens.model.exceptoin.FindTokenException
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.managetokens.utils.list.CustomTokenFormUseCasesFacade
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveInAndJoin
|
||||
|
|
@ -15,7 +19,11 @@ import kotlinx.coroutines.launch
|
|||
import timber.log.Timber
|
||||
|
||||
internal class CustomCurrencyValidator(
|
||||
private val userWalletId: UserWalletId,
|
||||
private val useCasesFacade: CustomTokenFormUseCasesFacade,
|
||||
private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase,
|
||||
private val findTokenUseCase: FindTokenUseCase,
|
||||
private val validateTokenFormUseCase: ValidateTokenFormUseCase,
|
||||
) {
|
||||
|
||||
private val validateFormJobHolder = JobHolder()
|
||||
|
|
@ -41,10 +49,7 @@ internal class CustomCurrencyValidator(
|
|||
) = coroutineScope {
|
||||
updateStatus(Status.Validating)
|
||||
|
||||
val result = useCasesFacade.validateTokenFormUseCase(
|
||||
networkId = networkId,
|
||||
formValues = formValues,
|
||||
)
|
||||
val result = validateTokenFormUseCase(networkId = networkId, formValues = formValues)
|
||||
|
||||
val validatedForm = result.getOrElse { e ->
|
||||
updateStatus(Status.FormValidationException(e))
|
||||
|
|
@ -90,7 +95,8 @@ internal class CustomCurrencyValidator(
|
|||
|
||||
updateStatus(Status.SearchingToken)
|
||||
|
||||
val foundToken = useCasesFacade.findTokenUseCase(
|
||||
val foundToken = findTokenUseCase.invoke(
|
||||
userWalletId = userWalletId,
|
||||
contractAddress = validatedForm.contractAddress,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
|
|
@ -121,7 +127,8 @@ internal class CustomCurrencyValidator(
|
|||
) {
|
||||
updateStatus(Status.SearchingToken)
|
||||
|
||||
val token = useCasesFacade.findTokenUseCase(
|
||||
val token = findTokenUseCase.invoke(
|
||||
userWalletId = userWalletId,
|
||||
contractAddress = validatedForm.contractAddress,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
|
|
@ -148,7 +155,8 @@ internal class CustomCurrencyValidator(
|
|||
derivationPath: Network.DerivationPath,
|
||||
validatedForm: AddCustomTokenForm.Validated.All?,
|
||||
) {
|
||||
val currency = useCasesFacade.createCryptoCurrencyUseCase(
|
||||
val currency = createCryptoCurrencyUseCase.invoke(
|
||||
userWalletId = userWalletId,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
formValues = validatedForm,
|
||||
|
|
|
|||
|
|
@ -1,54 +1,52 @@
|
|||
package com.tangem.features.managetokens.utils.list
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.NonEmptyList
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.domain.account.producer.SingleAccountProducer
|
||||
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.account.supplier.SingleAccountSupplier
|
||||
import com.tangem.domain.managetokens.CheckIsCurrencyNotAddedUseCase
|
||||
import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase
|
||||
import com.tangem.domain.managetokens.FindTokenUseCase
|
||||
import com.tangem.domain.managetokens.ValidateTokenFormUseCase
|
||||
import com.tangem.domain.managetokens.model.AddCustomTokenForm
|
||||
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
|
||||
import com.tangem.domain.managetokens.model.exceptoin.FindTokenException
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
|
||||
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
|
||||
import com.tangem.features.managetokens.component.AddCustomTokenMode
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
|
||||
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
private val validateTokenFormUseCase: ValidateTokenFormUseCase,
|
||||
private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase,
|
||||
private val findTokenUseCase: FindTokenUseCase,
|
||||
private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase,
|
||||
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
|
||||
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||
private val singleAccountSupplier: SingleAccountSupplier,
|
||||
@Assisted private val mode: AddCustomTokenMode,
|
||||
) {
|
||||
|
||||
suspend fun needColdWalletInteraction(network: Map<String, String?>): Boolean = when (mode) {
|
||||
is AddCustomTokenMode.Account -> TODO("Account")
|
||||
is AddCustomTokenMode.Wallet -> coldWalletAndHasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
networksWithDerivationPath = network,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun addCryptoCurrenciesUseCase(currency: CryptoCurrency): Either<Throwable, Unit> = when (mode) {
|
||||
is AddCustomTokenMode.Account -> TODO("Account")
|
||||
is AddCustomTokenMode.Wallet -> addCryptoCurrenciesUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
currency = currency,
|
||||
)
|
||||
is AddCustomTokenMode.Account -> {
|
||||
manageCryptoCurrenciesUseCase(
|
||||
accountId = AccountId.forCryptoPortfolio(
|
||||
userWalletId = mode.userWalletId,
|
||||
derivationIndex = DerivationIndex.Main,
|
||||
),
|
||||
add = currency,
|
||||
)
|
||||
}
|
||||
is AddCustomTokenMode.Wallet -> {
|
||||
addCryptoCurrenciesUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
currency = currency,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun derivePublicKeysUseCase(currencies: List<CryptoCurrency>): Either<Throwable, Unit> = when (mode) {
|
||||
is AddCustomTokenMode.Account -> TODO("Account")
|
||||
is AddCustomTokenMode.Account -> Unit.right()
|
||||
is AddCustomTokenMode.Wallet -> derivePublicKeysUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
currencies = currencies,
|
||||
|
|
@ -60,7 +58,19 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
|
|||
derivationPath: Network.DerivationPath,
|
||||
contractAddress: String?,
|
||||
): Either<Throwable, Boolean> = when (mode) {
|
||||
is AddCustomTokenMode.Account -> TODO("Account")
|
||||
is AddCustomTokenMode.Account -> {
|
||||
val account = singleAccountSupplier.getSyncOrNull(
|
||||
params = SingleAccountProducer.Params(accountId = mode.accountId),
|
||||
)
|
||||
?: return IllegalStateException("Account not found").left()
|
||||
|
||||
account.cryptoCurrencies.none { currency ->
|
||||
networkId == currency.network.id &&
|
||||
derivationPath == currency.network.derivationPath &&
|
||||
contractAddress.equals(currency.id.contractAddress, ignoreCase = true)
|
||||
}
|
||||
.right()
|
||||
}
|
||||
is AddCustomTokenMode.Wallet -> checkIsCurrencyNotAddedUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
networkId = networkId,
|
||||
|
|
@ -69,45 +79,6 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun createCryptoCurrencyUseCase(
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
formValues: AddCustomTokenForm.Validated.All?,
|
||||
): Either<Throwable, CryptoCurrency> = when (mode) {
|
||||
is AddCustomTokenMode.Account -> TODO("Account")
|
||||
is AddCustomTokenMode.Wallet -> createCryptoCurrencyUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
formValues = formValues,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun findTokenUseCase(
|
||||
contractAddress: String,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): Either<FindTokenException, CryptoCurrency.Token> = when (mode) {
|
||||
is AddCustomTokenMode.Account -> TODO("Account")
|
||||
is AddCustomTokenMode.Wallet -> findTokenUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
contractAddress = contractAddress,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun validateTokenFormUseCase(
|
||||
networkId: Network.ID,
|
||||
formValues: AddCustomTokenForm.Raw,
|
||||
): Either<NonEmptyList<CustomTokenFormValidationException>, AddCustomTokenForm.Validated> = when (mode) {
|
||||
is AddCustomTokenMode.Account -> TODO("Account")
|
||||
is AddCustomTokenMode.Wallet -> validateTokenFormUseCase.invoke(
|
||||
networkId = networkId,
|
||||
formValues = formValues,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(mode: AddCustomTokenMode): CustomTokenFormUseCasesFacade
|
||||
|
|
|
|||
|
|
@ -90,8 +90,9 @@ internal class ManageTokensListManager @AssistedInject constructor(
|
|||
*/
|
||||
suspend fun launchPagination(isCollapsed: Boolean) = coroutineScope {
|
||||
val loadUserTokensFromRemote = when (mode) {
|
||||
is ManageTokensMode.Wallet -> source == ManageTokensSource.ONBOARDING
|
||||
is ManageTokensMode.Wallet,
|
||||
is ManageTokensMode.Account,
|
||||
-> source == ManageTokensSource.ONBOARDING
|
||||
ManageTokensMode.None,
|
||||
-> false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,17 @@ package com.tangem.features.managetokens.utils.list
|
|||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.producer.SingleAccountProducer
|
||||
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.account.supplier.SingleAccountSupplier
|
||||
import com.tangem.domain.managetokens.*
|
||||
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
|
||||
import com.tangem.domain.managetokens.model.ManageTokensListConfig
|
||||
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
|
||||
import com.tangem.domain.managetokens.repository.CustomTokensRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
|
||||
|
|
@ -23,6 +30,10 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor(
|
|||
private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase,
|
||||
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
|
||||
private val saveManagedTokensUseCase: SaveManagedTokensUseCase,
|
||||
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||
private val customTokensRepository: CustomTokensRepository,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val singleAccountSupplier: SingleAccountSupplier,
|
||||
@Assisted private val mode: ManageTokensMode,
|
||||
) {
|
||||
|
||||
|
|
@ -30,17 +41,33 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor(
|
|||
get() = IllegalStateException("Unsupported")
|
||||
|
||||
fun manageTokensListConfig(searchText: String?): ManageTokensListConfig {
|
||||
val userWalletId: UserWalletId? = when (mode) {
|
||||
is ManageTokensMode.Account -> TODO("Account")
|
||||
ManageTokensMode.None -> null
|
||||
is ManageTokensMode.Wallet -> mode.userWalletId
|
||||
return when (mode) {
|
||||
is ManageTokensMode.Account -> {
|
||||
ManageTokensListConfig.Account(accountId = mode.accountId, searchText = searchText)
|
||||
}
|
||||
is ManageTokensMode.Wallet -> {
|
||||
ManageTokensListConfig.Wallet(userWalletId = mode.userWalletId, searchText = searchText)
|
||||
}
|
||||
ManageTokensMode.None -> {
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
ManageTokensListConfig.Account(accountId = null, searchText = searchText)
|
||||
} else {
|
||||
ManageTokensListConfig.Wallet(userWalletId = null, searchText = searchText)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ManageTokensListConfig(userWalletId, searchText)
|
||||
}
|
||||
|
||||
suspend fun removeCustomCurrencyUseCase(customCurrency: ManagedCryptoCurrency.Custom): Either<Throwable, Unit> {
|
||||
return when (mode) {
|
||||
is ManageTokensMode.Account -> TODO("Account")
|
||||
is ManageTokensMode.Account -> {
|
||||
val currency = customTokensRepository.convertToCryptoCurrency(
|
||||
userWalletId = mode.accountId.userWalletId,
|
||||
currency = customCurrency,
|
||||
)
|
||||
|
||||
manageCryptoCurrenciesUseCase(accountId = mode.accountId, remove = currency)
|
||||
}
|
||||
is ManageTokensMode.Wallet -> removeCustomCurrencyUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
customCurrency = customCurrency,
|
||||
|
|
@ -55,7 +82,21 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor(
|
|||
tempRemovedTokens: Map<ManagedCryptoCurrency.Token, Set<Network>>,
|
||||
): Either<Throwable, Boolean> {
|
||||
return when (mode) {
|
||||
is ManageTokensMode.Account -> TODO("Account")
|
||||
is ManageTokensMode.Account -> {
|
||||
val added = tempAddedTokens.mapToCryptoCurrencies(userWalletId = mode.accountId.userWalletId)
|
||||
val removed = tempRemovedTokens.mapToCryptoCurrencies(userWalletId = mode.accountId.userWalletId)
|
||||
|
||||
val account = singleAccountSupplier.getSyncOrNull(
|
||||
params = SingleAccountProducer.Params(accountId = mode.accountId),
|
||||
)
|
||||
?: return IllegalStateException("Account not found").left()
|
||||
|
||||
(account.cryptoCurrencies + added - removed).any {
|
||||
it is CryptoCurrency.Token && it.network.backendId == network.backendId &&
|
||||
it.network.derivationPath == network.derivationPath
|
||||
}
|
||||
.right()
|
||||
}
|
||||
is ManageTokensMode.Wallet -> checkHasLinkedTokensUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
network = network,
|
||||
|
|
@ -70,7 +111,10 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor(
|
|||
sourceNetwork: ManagedCryptoCurrency.SourceNetwork,
|
||||
): Either<Throwable, CurrencyUnsupportedState?> {
|
||||
return when (mode) {
|
||||
is ManageTokensMode.Account -> TODO("Account")
|
||||
is ManageTokensMode.Account -> checkCurrencyUnsupportedUseCase.invoke(
|
||||
userWalletId = mode.accountId.userWalletId,
|
||||
sourceNetwork = sourceNetwork,
|
||||
)
|
||||
is ManageTokensMode.Wallet -> checkCurrencyUnsupportedUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
sourceNetwork = sourceNetwork,
|
||||
|
|
@ -80,7 +124,10 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor(
|
|||
}
|
||||
|
||||
suspend fun needColdWalletInteraction(network: Map<String, Nothing?>): Boolean = when (mode) {
|
||||
is ManageTokensMode.Account -> TODO("Account")
|
||||
is ManageTokensMode.Account -> coldWalletAndHasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = mode.accountId.userWalletId,
|
||||
networksWithDerivationPath = network,
|
||||
)
|
||||
is ManageTokensMode.Wallet -> coldWalletAndHasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
networksWithDerivationPath = network,
|
||||
|
|
@ -92,15 +139,46 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor(
|
|||
currenciesToAdd: Map<ManagedCryptoCurrency.Token, Set<Network>>,
|
||||
currenciesToRemove: Map<ManagedCryptoCurrency.Token, Set<Network>>,
|
||||
): Either<Throwable, Unit> = when (mode) {
|
||||
is ManageTokensMode.Account -> TODO("Account")
|
||||
is ManageTokensMode.Wallet -> saveManagedTokensUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
currenciesToAdd = currenciesToAdd,
|
||||
currenciesToRemove = currenciesToRemove,
|
||||
)
|
||||
is ManageTokensMode.Account -> {
|
||||
manageCryptoCurrenciesUseCase(
|
||||
accountId = mode.accountId,
|
||||
add = currenciesToAdd.mapToCryptoCurrencies(userWalletId = mode.accountId.userWalletId),
|
||||
remove = currenciesToRemove.mapToCryptoCurrencies(userWalletId = mode.accountId.userWalletId),
|
||||
)
|
||||
}
|
||||
is ManageTokensMode.Wallet -> {
|
||||
saveManagedTokensUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
currenciesToAdd = currenciesToAdd,
|
||||
currenciesToRemove = currenciesToRemove,
|
||||
)
|
||||
}
|
||||
ManageTokensMode.None -> nonePortfolioError.left()
|
||||
}
|
||||
|
||||
private suspend fun Map<ManagedCryptoCurrency.Token, Set<Network>>.mapToCryptoCurrencies(
|
||||
userWalletId: UserWalletId,
|
||||
): List<CryptoCurrency> {
|
||||
return flatMap { (token, networks) ->
|
||||
token.availableNetworks
|
||||
.filter { sourceNetwork -> networks.contains(sourceNetwork.network) }
|
||||
.map { sourceNetwork ->
|
||||
when (sourceNetwork) {
|
||||
is ManagedCryptoCurrency.SourceNetwork.Default -> customTokensRepository.createToken(
|
||||
managedCryptoCurrency = token,
|
||||
sourceNetwork = sourceNetwork,
|
||||
rawId = CryptoCurrency.RawID(token.id.value),
|
||||
)
|
||||
is ManagedCryptoCurrency.SourceNetwork.Main -> customTokensRepository.createCoin(
|
||||
userWalletId = userWalletId,
|
||||
networkId = sourceNetwork.id,
|
||||
derivationPath = sourceNetwork.network.derivationPath,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(mode: ManageTokensMode): ManageTokensUseCasesFacade
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.tangem.core.decompose.context.child
|
|||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
|
||||
import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params
|
||||
|
|
@ -33,6 +34,7 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
|
|||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: Params,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
portfolioComponentFactory: MarketsPortfolioComponent.Factory,
|
||||
) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent {
|
||||
|
||||
|
|
@ -114,6 +116,7 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
|
|||
onBackClick = ::navigateBack,
|
||||
backButtonEnabled = bsState == BottomSheetState.EXPANDED,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
isAccountEnabled = accountsFeatureToggles.isFeatureEnabled,
|
||||
portfolioBlock = portfolioComponent?.let { component ->
|
||||
{ blockModifier ->
|
||||
component.Content(blockModifier)
|
||||
|
|
@ -141,6 +144,7 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
|
|||
onBackClick = ::navigateBack,
|
||||
backButtonEnabled = true,
|
||||
onHeaderSizeChange = {},
|
||||
isAccountEnabled = accountsFeatureToggles.isFeatureEnabled,
|
||||
portfolioBlock = portfolioComponent?.let { component ->
|
||||
{ blockModifier ->
|
||||
component.Content(blockModifier)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ internal fun MarketsTokenDetailsContent(
|
|||
onBackClick: () -> Unit,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
backButtonEnabled: Boolean,
|
||||
isAccountEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
|
|
@ -69,6 +70,7 @@ internal fun MarketsTokenDetailsContent(
|
|||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
backButtonEnabled = backButtonEnabled,
|
||||
portfolioBlock = portfolioBlock,
|
||||
isAccountEnabled = isAccountEnabled,
|
||||
addTopBarStatusBarInsets = addTopBarStatusBarPadding,
|
||||
)
|
||||
|
||||
|
|
@ -88,6 +90,7 @@ private fun Content(
|
|||
onBackClick: () -> Unit,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
backButtonEnabled: Boolean,
|
||||
isAccountEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
|
|
@ -153,6 +156,7 @@ private fun Content(
|
|||
|
||||
tokenMarketDetailsBody(
|
||||
state = state.body,
|
||||
isAccountEnabled = isAccountEnabled,
|
||||
portfolioBlock = portfolioBlock,
|
||||
)
|
||||
}
|
||||
|
|
@ -348,6 +352,7 @@ private fun Preview() {
|
|||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
portfolioBlock = {},
|
||||
backButtonEnabled = true,
|
||||
isAccountEnabled = true,
|
||||
addTopBarStatusBarPadding = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,17 +4,21 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.items.DescriptionItem
|
||||
import com.tangem.core.ui.components.items.DescriptionPlaceholder
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.impl.R
|
||||
|
||||
internal fun LazyListScope.tokenMarketDetailsBody(
|
||||
state: MarketsTokenDetailsUM.Body,
|
||||
isAccountEnabled: Boolean,
|
||||
portfolioBlock: @Composable ((Modifier) -> Unit)?,
|
||||
) {
|
||||
when (state) {
|
||||
|
|
@ -29,6 +33,10 @@ internal fun LazyListScope.tokenMarketDetailsBody(
|
|||
}
|
||||
}
|
||||
|
||||
if (isAccountEnabled) {
|
||||
aboutCoinHeader()
|
||||
}
|
||||
|
||||
loadingInfoBlocks()
|
||||
}
|
||||
is MarketsTokenDetailsUM.Body.Content -> {
|
||||
|
|
@ -42,6 +50,10 @@ internal fun LazyListScope.tokenMarketDetailsBody(
|
|||
}
|
||||
}
|
||||
|
||||
if (isAccountEnabled) {
|
||||
aboutCoinHeader()
|
||||
}
|
||||
|
||||
infoBlocksList(state.infoBlocks)
|
||||
}
|
||||
is MarketsTokenDetailsUM.Body.Error -> {
|
||||
|
|
@ -69,6 +81,21 @@ private fun LazyListScope.error(state: MarketsTokenDetailsUM.Body.Error) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.aboutCoinHeader() {
|
||||
item("aboutCoinHeader") {
|
||||
Text(
|
||||
modifier = Modifier.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing20,
|
||||
),
|
||||
text = stringResourceSafe(R.string.markets_about_coin_header),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.description(description: MarketsTokenDetailsUM.Description) {
|
||||
item("description") {
|
||||
DescriptionItem(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.markets.portfolio.add.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
|
||||
internal interface AddToPortfolioComponent : ComposableBottomSheetComponent {
|
||||
|
||||
data class Params(
|
||||
val addToPortfolioManager: AddToPortfolioManager,
|
||||
val callback: Callback,
|
||||
)
|
||||
|
||||
interface Callback {
|
||||
fun onDismiss()
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, AddToPortfolioComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.markets.portfolio.add.api
|
||||
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.account.PortfolioFetcher
|
||||
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent.AnalyticsParams
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
internal interface AddToPortfolioManager {
|
||||
|
||||
val token: TokenMarketParams
|
||||
val analyticsParams: AnalyticsParams?
|
||||
val portfolioFetcher: PortfolioFetcher
|
||||
|
||||
val state: StateFlow<State>
|
||||
|
||||
val allAvailableNetworks: Flow<List<TokenMarketInfo.Network>>
|
||||
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>)
|
||||
|
||||
sealed interface State {
|
||||
data object Init : State
|
||||
data class AvailableToAdd(
|
||||
val availableToAddData: AvailableToAddData,
|
||||
) : State
|
||||
|
||||
data object NothingToAdd : State
|
||||
}
|
||||
|
||||
interface Factory {
|
||||
fun create(
|
||||
scope: CoroutineScope,
|
||||
token: TokenMarketParams,
|
||||
analyticsParams: AnalyticsParams?,
|
||||
): AddToPortfolioManager
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
internal data class AvailableToAddData(
|
||||
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet>,
|
||||
|
|
@ -19,11 +20,12 @@ internal data class AvailableToAddData(
|
|||
|
||||
internal data class AvailableToAddWallet(
|
||||
val userWallet: UserWallet,
|
||||
val accounts: Set<AccountStatus>,
|
||||
val accounts: List<AccountStatus>,
|
||||
val availableNetworks: Set<TokenMarketInfo.Network>,
|
||||
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class AvailableToAddAccount(
|
||||
val account: AccountStatus,
|
||||
val availableNetworks: Set<TokenMarketInfo.Network>,
|
||||
|
|
@ -41,6 +43,7 @@ internal data class AvailableToAddAccount(
|
|||
.toSet()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class SelectedPortfolio(
|
||||
val userWallet: UserWallet,
|
||||
val account: AvailableToAddAccount,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.markets.portfolio.add.api.SelectedNetwork
|
||||
import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio
|
||||
|
|
@ -38,7 +37,6 @@ internal class AddTokenComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
data class Params(
|
||||
val marketParams: TokenMarketParams,
|
||||
val eventBuilder: PortfolioAnalyticsEvent.EventBuilder,
|
||||
val selectedPortfolio: Flow<SelectedPortfolio>,
|
||||
val selectedNetwork: Flow<SelectedNetwork>,
|
||||
|
|
|
|||
|
|
@ -1,48 +1,36 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl
|
||||
|
||||
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.factory.ComponentFactory
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio
|
||||
import com.tangem.features.markets.portfolio.add.impl.model.ChooseNetworkModel
|
||||
import com.tangem.features.markets.portfolio.add.impl.ui.ChooseNetworkContent
|
||||
import com.tangem.features.markets.portfolio.add.impl.ui.state.ChooseNetworkUM
|
||||
import com.tangem.features.markets.portfolio.impl.model.BlockchainRowUMConverter
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class ChooseNetworkComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : AppComponentContext by context, ComposableContentComponent {
|
||||
|
||||
private val state by lazy {
|
||||
val converter = BlockchainRowUMConverter(
|
||||
alreadyAddedNetworks = params.alreadyAdded.mapTo(mutableSetOf()) { it.networkId },
|
||||
)
|
||||
val allAvailableNetworks = params.allAvailable.map { it to true }
|
||||
ChooseNetworkUM(
|
||||
networks = converter.convertList(allAvailableNetworks).toPersistentList(),
|
||||
onNetworkClick = onNetworkClick@{ row ->
|
||||
val network = params.allAvailable
|
||||
.find { it.networkId == row.id }
|
||||
?: return@onNetworkClick
|
||||
params.callbacks.onNetworkSelected(network)
|
||||
},
|
||||
)
|
||||
}
|
||||
private val model: ChooseNetworkModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
ChooseNetworkContent(state)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val alreadyAdded: Set<TokenMarketInfo.Network>,
|
||||
val allAvailable: List<TokenMarketInfo.Network>,
|
||||
val selectedPortfolio: SelectedPortfolio,
|
||||
val callbacks: Callbacks,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.arkivanov.decompose.router.stack.backStack
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.account.PortfolioSelectorComponent
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent
|
||||
import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent.Params
|
||||
import com.tangem.features.markets.portfolio.add.impl.model.AddToPortfolioModel
|
||||
import com.tangem.features.markets.portfolio.add.impl.model.AddToPortfolioRoutes
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
|
||||
addTokenComponentFactory: AddTokenComponent.Factory,
|
||||
tokenActionsComponentFactory: TokenActionsComponent.Factory,
|
||||
private val chooseNetworkComponentFactory: ChooseNetworkComponent.Factory,
|
||||
) : AppComponentContext by context, AddToPortfolioComponent {
|
||||
|
||||
private val model: AddToPortfolioModel = getOrCreateModel(params)
|
||||
|
||||
private val portfolioSelectorComponent: PortfolioSelectorComponent = portfolioSelectorComponentFactory.create(
|
||||
context = child("portfolioSelectorComponent"),
|
||||
params = PortfolioSelectorComponent.Params(
|
||||
portfolioFetcher = model.portfolioFetcher,
|
||||
controller = model.portfolioSelectorController,
|
||||
),
|
||||
)
|
||||
|
||||
private val addTokenComponent: AddTokenComponent = addTokenComponentFactory.create(
|
||||
context = child("addTokenComponent"),
|
||||
params = AddTokenComponent.Params(
|
||||
eventBuilder = model.eventBuilder,
|
||||
callbacks = model,
|
||||
selectedPortfolio = model.selectedPortfolio,
|
||||
selectedNetwork = model.selectedNetwork,
|
||||
),
|
||||
)
|
||||
|
||||
private val tokenActionsComponent: TokenActionsComponent = tokenActionsComponentFactory.create(
|
||||
context = child("tokenActionsComponent"),
|
||||
params = TokenActionsComponent.Params(
|
||||
eventBuilder = model.eventBuilder,
|
||||
callbacks = model,
|
||||
data = model.tokenActionsData,
|
||||
),
|
||||
)
|
||||
|
||||
private val childStack = childStack(
|
||||
key = "addToPortfolioStack",
|
||||
handleBackButton = true,
|
||||
source = model.navigation,
|
||||
serializer = AddToPortfolioRoutes.serializer(),
|
||||
initialStack = { model.currentStack },
|
||||
childFactory = ::contentChild,
|
||||
)
|
||||
|
||||
private fun onBack() {
|
||||
if (childStack.backStack.isNotEmpty()) model.navigation.pop() else dismiss()
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
params.callback.onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val stack by childStack.subscribeAsState()
|
||||
val contentStack = remember { mutableStateOf(stack) }
|
||||
val currentRoute = stack.active.configuration
|
||||
val isNotEmpty = currentRoute != AddToPortfolioRoutes.Empty
|
||||
if (isNotEmpty) {
|
||||
contentStack.value = stack
|
||||
}
|
||||
|
||||
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
scrollableContent = false,
|
||||
onBack = ::onBack,
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = isNotEmpty,
|
||||
onDismissRequest = ::dismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
title = { state ->
|
||||
AnimatedContent(targetState = contentStack.value) { stack ->
|
||||
BottomSheetTitle(
|
||||
stack = stack,
|
||||
onBackClick = ::onBack,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { state ->
|
||||
AnimatedContent(targetState = contentStack.value) { stack ->
|
||||
val paddingModifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
)
|
||||
val scrollableContent = when (stack.active.configuration) {
|
||||
AddToPortfolioRoutes.PortfolioSelector -> false
|
||||
AddToPortfolioRoutes.AddToken,
|
||||
AddToPortfolioRoutes.Empty,
|
||||
is AddToPortfolioRoutes.NetworkSelector,
|
||||
AddToPortfolioRoutes.TokenActions,
|
||||
-> true
|
||||
}
|
||||
if (scrollableContent) {
|
||||
Column(
|
||||
modifier = paddingModifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
stack.active.instance.Content(modifier = Modifier)
|
||||
}
|
||||
} else {
|
||||
stack.active.instance.Content(modifier = paddingModifier)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSheetTitle(
|
||||
stack: ChildStack<AddToPortfolioRoutes, ComposableContentComponent>,
|
||||
onBackClick: (() -> Unit),
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val title: TextReference = when (stack.active.configuration) {
|
||||
AddToPortfolioRoutes.AddToken -> resourceReference(R.string.common_add_token)
|
||||
AddToPortfolioRoutes.Empty -> TextReference.EMPTY
|
||||
is AddToPortfolioRoutes.NetworkSelector -> resourceReference(R.string.common_choose_network)
|
||||
AddToPortfolioRoutes.TokenActions -> resourceReference(R.string.common_get_token)
|
||||
AddToPortfolioRoutes.PortfolioSelector -> (stack.active.instance as PortfolioSelectorComponent)
|
||||
.title.collectAsStateWithLifecycle().value
|
||||
}
|
||||
val startIconRes: Int?
|
||||
val endIconRes: Int?
|
||||
if (stack.backStack.isNotEmpty()) {
|
||||
startIconRes = R.drawable.ic_back_24
|
||||
endIconRes = null
|
||||
} else {
|
||||
startIconRes = null
|
||||
endIconRes = R.drawable.ic_close_24
|
||||
}
|
||||
TangemModalBottomSheetTitle(
|
||||
modifier = modifier,
|
||||
title = title,
|
||||
startIconRes = startIconRes,
|
||||
endIconRes = endIconRes,
|
||||
onStartClick = onBackClick,
|
||||
onEndClick = onBackClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun contentChild(
|
||||
config: AddToPortfolioRoutes,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableContentComponent = when (config) {
|
||||
AddToPortfolioRoutes.AddToken -> addTokenComponent
|
||||
AddToPortfolioRoutes.PortfolioSelector -> portfolioSelectorComponent
|
||||
AddToPortfolioRoutes.TokenActions -> tokenActionsComponent
|
||||
AddToPortfolioRoutes.Empty -> ComposableContentComponent.EMPTY
|
||||
is AddToPortfolioRoutes.NetworkSelector -> chooseNetworkComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = ChooseNetworkComponent.Params(
|
||||
selectedPortfolio = config.selectedPortfolio,
|
||||
callbacks = model,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : AddToPortfolioComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: Params): DefaultAddToPortfolioComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase
|
|||
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -28,11 +29,19 @@ internal class AvailableToAddDataConverter @Inject constructor(
|
|||
marketParams: TokenMarketParams,
|
||||
): AvailableToAddData {
|
||||
suspend fun AccountStatus.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount {
|
||||
val addedNetworks = availableNetworks
|
||||
.mapNotNull { createCryptoCurrency(wallet, it, marketParams) }
|
||||
.mapNotNull { getAccountCurrencyStatusUseCase.invokeSync(wallet.walletId, it) }
|
||||
.mapNotNull { it.getOrNull()?.status?.currency?.network }
|
||||
.toSet()
|
||||
val currencies = availableNetworks
|
||||
.mapNotNull { createCryptoCurrency(wallet, it, marketParams, this.account) }
|
||||
|
||||
val addedNetworks = getAccountCurrencyStatusUseCase.invokeSync(wallet.walletId, currencies)
|
||||
.fold(
|
||||
ifEmpty = { emptySet() },
|
||||
ifSome = { map ->
|
||||
map.values.flatMapTo(hashSetOf()) { statuses ->
|
||||
statuses.map { it.currency.network }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return AvailableToAddAccount(
|
||||
account = this,
|
||||
availableNetworks = availableNetworks,
|
||||
|
|
@ -60,7 +69,7 @@ internal class AvailableToAddDataConverter @Inject constructor(
|
|||
|
||||
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet> = balances
|
||||
.map {
|
||||
val (wallet, balance) = it
|
||||
val (wallet, _) = it
|
||||
val availableToAddWallet = getAvailableToAddWallet(it)
|
||||
wallet.walletId to availableToAddWallet
|
||||
}
|
||||
|
|
@ -82,9 +91,16 @@ internal class AvailableToAddDataConverter @Inject constructor(
|
|||
userWallet: UserWallet,
|
||||
network: TokenMarketInfo.Network,
|
||||
marketParams: TokenMarketParams,
|
||||
): CryptoCurrency? = getTokenMarketCryptoCurrency(
|
||||
userWalletId = userWallet.walletId,
|
||||
tokenMarketParams = marketParams,
|
||||
network = network,
|
||||
)
|
||||
account: Account,
|
||||
): CryptoCurrency? {
|
||||
val derivationIndex = when (account) {
|
||||
is Account.CryptoPortfolio -> account.derivationIndex
|
||||
}
|
||||
return getTokenMarketCryptoCurrency(
|
||||
userWalletId = userWallet.walletId,
|
||||
tokenMarketParams = marketParams,
|
||||
network = network,
|
||||
accountIndex = derivationIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.di
|
||||
|
||||
import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent
|
||||
import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager
|
||||
import com.tangem.features.markets.portfolio.add.impl.DefaultAddToPortfolioComponent
|
||||
import com.tangem.features.markets.portfolio.add.impl.ui.DefaultAddToPortfolioManager
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface AddToPortfolioComponentModule {
|
||||
|
||||
@Binds
|
||||
fun bindAddToPortfolioComponent(factory: DefaultAddToPortfolioComponent.Factory): AddToPortfolioComponent.Factory
|
||||
|
||||
@Binds
|
||||
fun bindAddToPortfolioManagerFactory(factory: DefaultAddToPortfolioManager.Factory): AddToPortfolioManager.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.markets.portfolio.add.impl.model.AddToPortfolioModel
|
||||
import com.tangem.features.markets.portfolio.add.impl.model.AddTokenModel
|
||||
import com.tangem.features.markets.portfolio.add.impl.model.ChooseNetworkModel
|
||||
import com.tangem.features.markets.portfolio.add.impl.model.TokenActionsModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface AddToPortfolioModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(AddTokenModel::class)
|
||||
fun addTokenModel(model: AddTokenModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(AddToPortfolioModel::class)
|
||||
fun addToPortfolioModel(model: AddToPortfolioModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TokenActionsModel::class)
|
||||
fun tokenActionsModel(model: TokenActionsModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(ChooseNetworkModel::class)
|
||||
fun chooseNetworkModel(model: ChooseNetworkModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,358 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.model
|
||||
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.popToFirst
|
||||
import com.arkivanov.decompose.router.stack.pushNew
|
||||
import com.arkivanov.decompose.router.stack.replaceAll
|
||||
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.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.ToastMessage
|
||||
import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2
|
||||
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.features.account.PortfolioSelectorController
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.portfolio.add.api.*
|
||||
import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent
|
||||
import com.tangem.features.markets.portfolio.add.impl.ChooseNetworkComponent
|
||||
import com.tangem.features.markets.portfolio.add.impl.TokenActionsComponent
|
||||
import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent
|
||||
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.markets.portfolio.impl.model.PortfolioTokenUMConverter.Companion.toQuickActions
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TOKEN_ACTIONS_DELAY = 500L
|
||||
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class AddToPortfolioModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val callbackDelegate: AddToPortfolioCallbackDelegate,
|
||||
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2,
|
||||
private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency,
|
||||
private val messageSender: UiMessageSender,
|
||||
val portfolioSelectorController: PortfolioSelectorController,
|
||||
) : Model(),
|
||||
ChooseNetworkComponent.Callbacks by callbackDelegate,
|
||||
TokenActionsComponent.Callbacks by callbackDelegate,
|
||||
AddTokenComponent.Callbacks by callbackDelegate {
|
||||
|
||||
private val params = paramsContainer.require<AddToPortfolioComponent.Params>()
|
||||
val navigation = StackNavigation<AddToPortfolioRoutes>()
|
||||
var currentStack = listOf<AddToPortfolioRoutes>(AddToPortfolioRoutes.Empty)
|
||||
|
||||
/* Flows that hold state and provide it to child models */
|
||||
val selectedNetwork: MutableSharedFlow<SelectedNetwork> = replayMutableSharedFlow()
|
||||
val selectedPortfolio: MutableSharedFlow<SelectedPortfolio> = replayMutableSharedFlow()
|
||||
val tokenActionsData: MutableSharedFlow<PortfolioData.CryptoCurrencyData> = replayMutableSharedFlow()
|
||||
|
||||
private val addToPortfolioManager = params.addToPortfolioManager
|
||||
val portfolioFetcher = addToPortfolioManager.portfolioFetcher
|
||||
val eventBuilder = PortfolioAnalyticsEvent.EventBuilder(
|
||||
token = addToPortfolioManager.token,
|
||||
source = addToPortfolioManager.analyticsParams?.source,
|
||||
)
|
||||
|
||||
val featureData: Flow<AddToPortfolioManager.State> = combineFeatureData()
|
||||
|
||||
init {
|
||||
navigation.subscribe { currentStack = it.transformer.invoke(currentStack) }
|
||||
startAddToPortfolioFlow()
|
||||
}
|
||||
|
||||
private fun <T> replayMutableSharedFlow() = MutableSharedFlow<T>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun startAddToPortfolioFlow() {
|
||||
channelFlow<Unit> {
|
||||
fun finishFlow() {
|
||||
params.callback.onDismiss()
|
||||
channel.close()
|
||||
}
|
||||
val featureDataFlow: StateFlow<AvailableToAddData> = featureData
|
||||
.filterIsInstance<AddToPortfolioManager.State.AvailableToAdd>()
|
||||
.map { it.availableToAddData }
|
||||
.distinctUntilChanged()
|
||||
.stateIn(this)
|
||||
|
||||
// use snapshot data, looks like we don’t need to remap at runtime
|
||||
val data = featureDataFlow.value
|
||||
|
||||
// you must control it via [AddToPortfolioManager.state]
|
||||
if (!data.availableToAdd) {
|
||||
finishFlow()
|
||||
return@channelFlow
|
||||
}
|
||||
|
||||
// init data flows, emits on user/code selection, updates state holder
|
||||
val firstSelectedPortfolio = setupPortfolioFlow(data)
|
||||
.onEach { selectedPortfolio.emit(it) }
|
||||
val firstSelectedNetwork = setupNetworkFlow(firstSelectedPortfolio)
|
||||
.onEach { selectedNetwork.emit(it) }
|
||||
|
||||
val isSinglePortfolio = data.isSinglePortfolio
|
||||
if (isSinglePortfolio) {
|
||||
val accountId = data.availableToAddWallets.values.first()
|
||||
.availableToAddAccounts.values.first()
|
||||
.account.account.accountId
|
||||
// force select a portfolio, triggers [selectedPortfolio]
|
||||
portfolioSelectorController.selectAccount(accountId)
|
||||
} else {
|
||||
navigation.replaceAll(AddToPortfolioRoutes.PortfolioSelector)
|
||||
}
|
||||
|
||||
val firstPartOfNavigation: Job = firstSelectedPortfolio
|
||||
.onEach { portfolio ->
|
||||
val isSingleAvailableNetwork = portfolio.account.isSingleNetwork
|
||||
when {
|
||||
// force select a network, triggers [selectedNetwork]
|
||||
isSingleAvailableNetwork -> {
|
||||
val singleNetwork = portfolio.account.availableToAddNetworks.first()
|
||||
callbackDelegate.onNetworkSelected(singleNetwork)
|
||||
}
|
||||
// it's important to control root screen, UI depends on it(close/arrow icon)
|
||||
isSinglePortfolio -> navigation.replaceAll(routeToNetworkSelector(portfolio))
|
||||
else -> navigation.pushNew(routeToNetworkSelector(portfolio))
|
||||
}
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
// main flow that combine all require data
|
||||
val allRequireForAdd = combine(
|
||||
flow = firstSelectedNetwork,
|
||||
flow2 = firstSelectedPortfolio,
|
||||
transform = { a, b -> a to b },
|
||||
)
|
||||
|
||||
// suspend until all required data is selected
|
||||
allRequireForAdd.first()
|
||||
// line of navigation to AddToken screen is finished; cancel the job, select a new root screen
|
||||
firstPartOfNavigation.cancel()
|
||||
navigation.replaceAll(AddToPortfolioRoutes.AddToken)
|
||||
|
||||
var middleNavigationJob: Job? = null
|
||||
// handle actions from AddToken screen
|
||||
callbackDelegate.onChangeNetworkClick.receiveAsFlow()
|
||||
.onEach {
|
||||
middleNavigationJob?.cancel()
|
||||
middleNavigationJob = changeNetworkNavigationFlow()
|
||||
.launchIn(this)
|
||||
val route = routeToNetworkSelector(selectedPortfolio.first())
|
||||
navigation.pushNew(route)
|
||||
}
|
||||
.launchIn(this)
|
||||
// handle actions from AddToken screen
|
||||
callbackDelegate.onChangePortfolioClick.receiveAsFlow()
|
||||
.onEach {
|
||||
middleNavigationJob?.cancel()
|
||||
middleNavigationJob = changePortfolioNavigationFlow(data).launchIn(this)
|
||||
navigation.pushNew(AddToPortfolioRoutes.PortfolioSelector)
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
// suspend until token is added
|
||||
val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first()
|
||||
middleNavigationJob?.cancel()
|
||||
val selectedPortfolio = selectedPortfolio.first()
|
||||
|
||||
messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added)))
|
||||
|
||||
setupTokenActionsFlow(selectedPortfolio, addedToken)
|
||||
.onEach {
|
||||
tokenActionsData.emit(it)
|
||||
navigation.replaceAll(AddToPortfolioRoutes.TokenActions)
|
||||
}
|
||||
.onEmpty { finishFlow() }
|
||||
.launchIn(this)
|
||||
|
||||
callbackDelegate.onLaterClick.receiveAsFlow().first()
|
||||
finishFlow()
|
||||
}
|
||||
.catch {
|
||||
Timber.e(it)
|
||||
params.callback.onDismiss()
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun changeNetworkNavigationFlow(): Flow<SelectedNetwork> {
|
||||
return setupNetworkFlow(selectedPortfolio)
|
||||
.onEach { newNetwork ->
|
||||
selectedNetwork.emit(newNetwork)
|
||||
navigation.popToFirst()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun changePortfolioNavigationFlow(data: AvailableToAddData): Flow<Unit> {
|
||||
val selectedAccount = selectedPortfolio.first().account.account.account.accountId
|
||||
portfolioSelectorController.selectAccount(selectedAccount)
|
||||
val changedPortfolio = setupPortfolioFlow(data)
|
||||
.drop(1)
|
||||
.onEach { portfolio -> navigation.pushNew(routeToNetworkSelector(portfolio)) }
|
||||
val changedNetwork = setupNetworkFlow(changedPortfolio)
|
||||
return combine(
|
||||
flow = changedPortfolio,
|
||||
flow2 = changedNetwork,
|
||||
transform = { newPortfolio, newNetwork ->
|
||||
selectedPortfolio.tryEmit(newPortfolio)
|
||||
selectedNetwork.tryEmit(newNetwork)
|
||||
navigation.popToFirst()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupTokenActionsFlow(
|
||||
selectedPortfolio: SelectedPortfolio,
|
||||
addedToken: CryptoCurrencyStatus,
|
||||
): Flow<PortfolioData.CryptoCurrencyData> {
|
||||
val timeFlow = channelFlow {
|
||||
val timerJob = launch { delay(TOKEN_ACTIONS_DELAY) }
|
||||
getCryptoCurrencyActionsUseCase(
|
||||
currency = addedToken.currency,
|
||||
accountId = selectedPortfolio.account.account.account.accountId,
|
||||
).onEach { state ->
|
||||
val requestedQuickActions = toQuickActions(state.states)
|
||||
when {
|
||||
requestedQuickActions.isNotEmpty() -> {
|
||||
timerJob.cancel()
|
||||
send(state)
|
||||
}
|
||||
// wait any requestedQuickActions while timer active
|
||||
timerJob.isActive -> Unit
|
||||
else -> close()
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
return timeFlow.map {
|
||||
PortfolioData.CryptoCurrencyData(
|
||||
userWallet = selectedPortfolio.userWallet,
|
||||
status = addedToken,
|
||||
actions = it.states,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupPortfolioFlow(data: AvailableToAddData): Flow<SelectedPortfolio> = combine(
|
||||
flow = portfolioSelectorController.isAccountMode,
|
||||
flow2 = portfolioSelectorController.selectedAccount,
|
||||
transform = { isAccountMode, selectedAccountId ->
|
||||
selectedAccountId ?: return@combine null
|
||||
val availableToAddWallets =
|
||||
data.availableToAddWallets[selectedAccountId.userWalletId] ?: return@combine null
|
||||
val availableToAddAccount =
|
||||
availableToAddWallets.availableToAddAccounts[selectedAccountId] ?: return@combine null
|
||||
SelectedPortfolio(
|
||||
isAccountMode = isAccountMode,
|
||||
userWallet = availableToAddWallets.userWallet,
|
||||
account = availableToAddAccount,
|
||||
availableMorePortfolio = !data.isSinglePortfolio,
|
||||
)
|
||||
},
|
||||
)
|
||||
.filterNotNull()
|
||||
|
||||
private fun setupNetworkFlow(selectedPortfolioFlow: Flow<SelectedPortfolio>): Flow<SelectedNetwork> = combine(
|
||||
flow = selectedPortfolioFlow,
|
||||
flow2 = callbackDelegate.onNetworkSelected.receiveAsFlow(),
|
||||
transform = transform@{ selectedPortfolio, selectedNetwork ->
|
||||
SelectedNetwork(
|
||||
cryptoCurrency = createCryptoCurrency(
|
||||
userWallet = selectedPortfolio.userWallet,
|
||||
network = selectedNetwork,
|
||||
account = selectedPortfolio.account,
|
||||
) ?: return@transform null,
|
||||
selectedNetwork = selectedNetwork,
|
||||
availableMoreNetwork = !selectedPortfolio.account.isSingleNetwork,
|
||||
)
|
||||
},
|
||||
)
|
||||
.filterNotNull()
|
||||
|
||||
private suspend fun createCryptoCurrency(
|
||||
userWallet: UserWallet,
|
||||
network: TokenMarketInfo.Network,
|
||||
account: AvailableToAddAccount,
|
||||
): CryptoCurrency? {
|
||||
val accountIndex = when (account.account) {
|
||||
is AccountStatus.CryptoPortfolio -> account.account.account.derivationIndex
|
||||
}
|
||||
return getTokenMarketCryptoCurrency(
|
||||
userWalletId = userWallet.walletId,
|
||||
tokenMarketParams = addToPortfolioManager.token,
|
||||
network = network,
|
||||
accountIndex = accountIndex,
|
||||
)
|
||||
}
|
||||
|
||||
private fun routeToNetworkSelector(portfolio: SelectedPortfolio): AddToPortfolioRoutes.NetworkSelector {
|
||||
return AddToPortfolioRoutes.NetworkSelector(selectedPortfolio = portfolio)
|
||||
}
|
||||
|
||||
private fun combineFeatureData() = addToPortfolioManager.state.onEach { state ->
|
||||
when (state) {
|
||||
is AddToPortfolioManager.State.AvailableToAdd ->
|
||||
portfolioSelectorController.isEnabled.value = isEnabled@{ userWallet, accountStatus ->
|
||||
val availableWallet = state.availableToAddData.availableToAddWallets[userWallet.walletId]
|
||||
?: return@isEnabled false
|
||||
val availableAccount =
|
||||
availableWallet.availableToAddAccounts[accountStatus.account.accountId]
|
||||
return@isEnabled availableAccount != null
|
||||
}
|
||||
AddToPortfolioManager.State.Init,
|
||||
AddToPortfolioManager.State.NothingToAdd,
|
||||
-> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ModelScoped
|
||||
internal class AddToPortfolioCallbackDelegate @Inject constructor() :
|
||||
ChooseNetworkComponent.Callbacks,
|
||||
TokenActionsComponent.Callbacks,
|
||||
AddTokenComponent.Callbacks {
|
||||
|
||||
val onNetworkSelected = Channel<TokenMarketInfo.Network>()
|
||||
val onLaterClick = Channel<Unit>()
|
||||
val onChangeNetworkClick = Channel<Unit>()
|
||||
val onChangePortfolioClick = Channel<Unit>()
|
||||
val onTokenAdded = Channel<CryptoCurrencyStatus>()
|
||||
|
||||
override fun onNetworkSelected(network: TokenMarketInfo.Network) {
|
||||
onNetworkSelected.trySend(network)
|
||||
}
|
||||
|
||||
override fun onLaterClick() {
|
||||
onLaterClick.trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onChangeNetworkClick() {
|
||||
onChangeNetworkClick.trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onChangePortfolioClick() {
|
||||
onChangePortfolioClick.trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onTokenAdded(status: CryptoCurrencyStatus) {
|
||||
onTokenAdded.trySend(status)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
internal sealed interface AddToPortfolioRoutes : Route {
|
||||
|
||||
@Serializable
|
||||
data object Empty : AddToPortfolioRoutes
|
||||
|
||||
@Serializable
|
||||
data object PortfolioSelector : AddToPortfolioRoutes
|
||||
|
||||
@Serializable
|
||||
data class NetworkSelector(
|
||||
val selectedPortfolio: SelectedPortfolio,
|
||||
) : AddToPortfolioRoutes
|
||||
|
||||
@Serializable
|
||||
data object AddToken : AddToPortfolioRoutes
|
||||
|
||||
@Serializable
|
||||
data object TokenActions : AddToPortfolioRoutes
|
||||
}
|
||||
|
|
@ -4,9 +4,14 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
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.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.ToastMessage
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.SaveCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.portfolio.add.api.SelectedNetwork
|
||||
import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio
|
||||
import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent
|
||||
|
|
@ -25,10 +30,11 @@ import javax.inject.Inject
|
|||
internal class AddTokenModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val uiBuilder: AddTokenUiBuilder,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val saveCryptoCurrenciesUseCase: SaveCryptoCurrenciesUseCase,
|
||||
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -66,19 +72,26 @@ internal class AddTokenModel @Inject constructor(
|
|||
val blockchainNames = listOf(selectedNetwork.selectedNetwork)
|
||||
.mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name }
|
||||
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames))
|
||||
|
||||
val cryptoCurrency = selectedNetwork.cryptoCurrency
|
||||
val accountId = selectedPortfolio.account.account.account.accountId
|
||||
saveCryptoCurrenciesUseCase(
|
||||
accountId = accountId,
|
||||
add = listOf(cryptoCurrency),
|
||||
remove = listOf(),
|
||||
)
|
||||
manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency)
|
||||
.onLeft {
|
||||
processError(error = it)
|
||||
uiState.value = um.toggleProgress(false)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val status = getAccountCurrencyStatusUseCase.invokeSync(
|
||||
userWalletId = accountId.userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
network = cryptoCurrency.network,
|
||||
).getOrNull() ?: return@launch
|
||||
params.callbacks.onTokenAdded(status.status)
|
||||
).getOrNull()
|
||||
if (status == null) {
|
||||
processError(error = null)
|
||||
} else {
|
||||
params.callbacks.onTokenAdded(status.status)
|
||||
}
|
||||
uiState.value = um.toggleProgress(false)
|
||||
}
|
||||
|
||||
|
|
@ -89,4 +102,10 @@ internal class AddTokenModel @Inject constructor(
|
|||
userWalletId = selectedPortfolio.userWallet.walletId,
|
||||
networksWithDerivationPath = mapOf(selectedNetwork.selectedNetwork.networkId to null),
|
||||
)
|
||||
|
||||
private fun processError(error: Throwable?) {
|
||||
val message = error?.message?.let { stringReference(it) }
|
||||
?: resourceReference(R.string.common_something_went_wrong)
|
||||
messageSender.send(ToastMessage(message = message))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.model
|
||||
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconUM
|
||||
import com.tangem.common.ui.account.PortfolioSelectUM
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -33,7 +34,7 @@ internal class AddTokenUiBuilder @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun createPortfolio(selectedPortfolio: SelectedPortfolio): AddTokenUM.Portfolio {
|
||||
private fun createPortfolio(selectedPortfolio: SelectedPortfolio): PortfolioSelectUM {
|
||||
val accountIcon: CryptoPortfolioIconUM?
|
||||
val portfolioName: TextReference
|
||||
when (selectedPortfolio.isAccountMode) {
|
||||
|
|
@ -49,10 +50,11 @@ internal class AddTokenUiBuilder @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
return AddTokenUM.Portfolio(
|
||||
accountIconUM = accountIcon,
|
||||
return PortfolioSelectUM(
|
||||
icon = accountIcon,
|
||||
name = portfolioName,
|
||||
editable = selectedPortfolio.availableMorePortfolio,
|
||||
isAccountMode = selectedPortfolio.isAccountMode,
|
||||
isMultiChoice = selectedPortfolio.availableMorePortfolio,
|
||||
onClick = { params.callbacks.onChangePortfolioClick() },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
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.ui.UiMessageSender
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
|
||||
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.portfolio.add.impl.ChooseNetworkComponent
|
||||
import com.tangem.features.markets.portfolio.add.impl.ui.state.ChooseNetworkUM
|
||||
import com.tangem.features.markets.portfolio.impl.model.BlockchainRowUMConverter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class ChooseNetworkModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ChooseNetworkComponent.Params>()
|
||||
|
||||
val uiState: StateFlow<ChooseNetworkUM> = MutableStateFlow(buildUI())
|
||||
|
||||
private fun buildUI(): ChooseNetworkUM {
|
||||
val allAvailable = params.selectedPortfolio.account.availableNetworks
|
||||
val alreadyAdded = allAvailable
|
||||
.subtract(params.selectedPortfolio.account.availableToAddNetworks)
|
||||
val converter = BlockchainRowUMConverter(
|
||||
alreadyAddedNetworks = alreadyAdded.mapTo(mutableSetOf()) { it.networkId },
|
||||
)
|
||||
val allAvailableNetworks = allAvailable.map { it to true }
|
||||
return ChooseNetworkUM(
|
||||
networks = converter.convertList(allAvailableNetworks).toPersistentList(),
|
||||
onNetworkClick = onNetworkClick@{ row ->
|
||||
val network = allAvailable
|
||||
.find { it.networkId == row.id }
|
||||
?: return@onNetworkClick
|
||||
checkNetwork(row, network)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkNetwork(row: BlockchainRowUM, network: TokenMarketInfo.Network) = modelScope.launch {
|
||||
val selectedWalletId = params.selectedPortfolio.userWallet.walletId
|
||||
val unsupportedState = checkCurrencyUnsupportedState(
|
||||
userWalletId = selectedWalletId,
|
||||
rawNetworkId = row.id,
|
||||
isMainNetwork = row.isMainNetwork,
|
||||
)
|
||||
if (unsupportedState != null) {
|
||||
showUnsupportedWarning(unsupportedState)
|
||||
} else {
|
||||
params.callbacks.onNetworkSelected(network)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkCurrencyUnsupportedState(
|
||||
userWalletId: UserWalletId,
|
||||
rawNetworkId: String,
|
||||
isMainNetwork: Boolean,
|
||||
): CurrencyUnsupportedState? {
|
||||
return checkCurrencyUnsupportedUseCase(
|
||||
userWalletId = userWalletId,
|
||||
networkId = rawNetworkId,
|
||||
isMainNetwork = isMainNetwork,
|
||||
).getOrElse {
|
||||
Timber.e(
|
||||
it,
|
||||
"""
|
||||
Failed to check currency unsupported state
|
||||
|- User wallet ID: $userWalletId
|
||||
|- Network ID: $rawNetworkId
|
||||
|- Is main network: $isMainNetwork
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val message = SnackbarMessage(
|
||||
message = it.localizedMessage?.let(::stringReference) ?: resourceReference(R.string.common_error),
|
||||
)
|
||||
messageSender.send(message)
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) {
|
||||
val message = DialogMessage(
|
||||
message = when (unsupportedState) {
|
||||
is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_curve_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference(
|
||||
id = R.string.alert_manage_tokens_unsupported_curve_message,
|
||||
formatArgs = wrappedList(unsupportedState.networkName),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
messageSender.send(message)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,14 +22,14 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.account.AccountIcon
|
||||
import com.tangem.common.ui.account.AccountIconPreviewData
|
||||
import com.tangem.common.ui.account.PortfolioSelectRow
|
||||
import com.tangem.common.ui.account.PortfolioSelectUM
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.components.account.AccountIconSize
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.token.TokenItem
|
||||
|
|
@ -63,12 +63,11 @@ internal fun AddTokenContent(state: AddTokenUM, modifier: Modifier = Modifier) {
|
|||
|
||||
SpacerH(TangemTheme.dimens.spacing14)
|
||||
Column(
|
||||
modifier = Modifier.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius14),
|
||||
),
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
|
||||
.background(color = TangemTheme.colors.background.action),
|
||||
) {
|
||||
PortfolioRow(state.portfolio)
|
||||
PortfolioSelectRow(state.portfolio)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
thickness = TangemTheme.dimens.size0_5,
|
||||
|
|
@ -86,50 +85,6 @@ internal fun AddTokenContent(state: AddTokenUM, modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PortfolioRow(state: AddTokenUM.Portfolio, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clickable(enabled = state.editable, onClick = state.onClick)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val leftText = if (state.isAccountMode) R.string.account_details_title else R.string.wc_common_wallet
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResourceSafe(leftText),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerW12()
|
||||
if (state.accountIconUM != null) {
|
||||
AccountIcon(
|
||||
name = state.name,
|
||||
icon = state.accountIconUM,
|
||||
size = AccountIconSize.Small,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
text = state.name.resolveReference(),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
if (state.editable) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(width = 18.dp, height = 24.dp)
|
||||
.testTag(WalletConnectBottomSheetTestTags.NETWORKS_SELECTOR_ICON),
|
||||
painter = painterResource(id = R.drawable.ic_select_18_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetworkRow(state: AddTokenUM.Network, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
|
|
@ -240,17 +195,19 @@ private class PreviewProvider : PreviewParameterProvider<AddTokenUM> {
|
|||
)
|
||||
|
||||
val account
|
||||
get() = AddTokenUM.Portfolio(
|
||||
accountIconUM = AccountIconPreviewData.randomAccountIcon(),
|
||||
get() = PortfolioSelectUM(
|
||||
icon = AccountIconPreviewData.randomAccountIcon(),
|
||||
name = AccountName.DefaultMain.toUM().value,
|
||||
editable = true,
|
||||
isAccountMode = true,
|
||||
isMultiChoice = true,
|
||||
onClick = {},
|
||||
)
|
||||
val wallet
|
||||
get() = AddTokenUM.Portfolio(
|
||||
accountIconUM = null,
|
||||
name = stringReference("Wallet"),
|
||||
editable = true,
|
||||
get() = PortfolioSelectUM(
|
||||
icon = null,
|
||||
name = stringReference("Wallet Name"),
|
||||
isMultiChoice = false,
|
||||
isAccountMode = false,
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
|
|
@ -271,7 +228,7 @@ private class PreviewProvider : PreviewParameterProvider<AddTokenUM> {
|
|||
AddTokenUM(
|
||||
tokenToAdd = tokenState,
|
||||
network = networkUM.copy(editable = false),
|
||||
portfolio = wallet.copy(editable = false),
|
||||
portfolio = wallet.copy(isMultiChoice = false),
|
||||
button = button.copy(
|
||||
isEnabled = true,
|
||||
isTangemIconVisible = true,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.ui
|
||||
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.account.PortfolioFetcher
|
||||
import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager
|
||||
import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager.State
|
||||
import com.tangem.features.markets.portfolio.add.impl.converter.AvailableToAddDataConverter
|
||||
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
internal class DefaultAddToPortfolioManager @AssistedInject constructor(
|
||||
private val availableToAddDataConverter: AvailableToAddDataConverter,
|
||||
@Assisted override val token: TokenMarketParams,
|
||||
@Assisted override val analyticsParams: MarketsPortfolioComponent.AnalyticsParams?,
|
||||
@Assisted val scope: CoroutineScope,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
portfolioFetcherFactory: PortfolioFetcher.Factory,
|
||||
) : AddToPortfolioManager {
|
||||
|
||||
private val _allAvailableNetworks = MutableSharedFlow<List<TokenMarketInfo.Network>>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
override val allAvailableNetworks: Flow<List<TokenMarketInfo.Network>> = _allAvailableNetworks.asSharedFlow()
|
||||
override val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create(
|
||||
mode = PortfolioFetcher.Mode.All(onlyMultiCurrency = true),
|
||||
scope = scope,
|
||||
)
|
||||
|
||||
override val state: StateFlow<State> =
|
||||
combine(
|
||||
flow = portfolioFetcher.data.map { it.balances }.distinctUntilChanged(),
|
||||
flow2 = allAvailableNetworks.map { it.toSet() }.distinctUntilChanged(),
|
||||
) { balances, availableNetworks ->
|
||||
val data = availableToAddDataConverter.convert(
|
||||
balances = balances,
|
||||
availableNetworks = availableNetworks,
|
||||
marketParams = token,
|
||||
)
|
||||
if (data.availableToAdd) {
|
||||
State.AvailableToAdd(data)
|
||||
} else {
|
||||
State.NothingToAdd
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = State.Init,
|
||||
)
|
||||
|
||||
override fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
_allAvailableNetworks.tryEmit(networks)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : AddToPortfolioManager.Factory {
|
||||
override fun create(
|
||||
scope: CoroutineScope,
|
||||
token: TokenMarketParams,
|
||||
analyticsParams: MarketsPortfolioComponent.AnalyticsParams?,
|
||||
): DefaultAddToPortfolioManager
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,16 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.ui.state
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconUM
|
||||
import com.tangem.common.ui.account.PortfolioSelectUM
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
data class AddTokenUM(
|
||||
val tokenToAdd: TokenItemState,
|
||||
val network: Network,
|
||||
val portfolio: Portfolio,
|
||||
val portfolio: PortfolioSelectUM,
|
||||
val button: Button,
|
||||
) {
|
||||
data class Portfolio(
|
||||
val accountIconUM: CryptoPortfolioIconUM?,
|
||||
val name: TextReference,
|
||||
val editable: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
) {
|
||||
val isAccountMode get() = accountIconUM != null
|
||||
}
|
||||
|
||||
data class Network(
|
||||
@DrawableRes val icon: Int,
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@ import com.tangem.core.decompose.context.childByContext
|
|||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent
|
||||
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel
|
||||
import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioRoute
|
||||
import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -28,19 +29,25 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor(
|
|||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: MarketsPortfolioComponent.Params,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
|
||||
) : AppComponentContext by context, MarketsPortfolioComponent {
|
||||
|
||||
private val model: MarketsPortfolioModel = getOrCreateModel(params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = TokenReceiveConfig.serializer(),
|
||||
serializer = MarketsPortfolioRoute.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
override fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) = model.setTokenNetworks(networks)
|
||||
override fun setNoNetworksAvailable() = model.setNoNetworksAvailable()
|
||||
override fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
model.setTokenNetworks(networks)
|
||||
}
|
||||
|
||||
override fun setNoNetworksAvailable() {
|
||||
model.setNoNetworksAvailable()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
|
|
@ -52,15 +59,24 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun bottomSheetChild(
|
||||
config: TokenReceiveConfig,
|
||||
config: MarketsPortfolioRoute,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = TokenReceiveComponent.Params(
|
||||
config = config,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
): ComposableBottomSheetComponent = when (config) {
|
||||
MarketsPortfolioRoute.AddToPortfolio -> addToPortfolioComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = AddToPortfolioComponent.Params(
|
||||
addToPortfolioManager = model.newAddToPortfolioManager!!,
|
||||
callback = model.addToPortfolioCallback,
|
||||
),
|
||||
)
|
||||
is MarketsPortfolioRoute.TokenReceive -> tokenReceiveComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = TokenReceiveComponent.Params(
|
||||
config = config.config,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MarketsPortfolioComponent.Factory {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
|
|||
import arrow.core.getOrElse
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
|
|
@ -16,15 +17,14 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
|
||||
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
|
||||
import com.tangem.domain.markets.SaveMarketTokensUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
|
|
@ -32,11 +32,13 @@ import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory
|
|||
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent
|
||||
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent
|
||||
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
|
||||
import com.tangem.features.wallet.utils.UserWalletImageFetcher
|
||||
|
|
@ -49,6 +51,7 @@ import kotlinx.coroutines.flow.*
|
|||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager as NewAddToPortfolioManager
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@Stable
|
||||
|
|
@ -69,6 +72,9 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
|
||||
private val userWalletImageFetcher: UserWalletImageFetcher,
|
||||
private val receiveAddressesFactory: ReceiveAddressesFactory,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
newAddToPortfolioManagerFactory: NewAddToPortfolioManager.Factory,
|
||||
private val newMarketsPortfolioDelegateFactory: NewMarketsPortfolioDelegate.Factory,
|
||||
) : Model() {
|
||||
|
||||
val state: StateFlow<MyPortfolioUM> get() = _state
|
||||
|
|
@ -80,12 +86,40 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
source = params.analyticsParams?.source,
|
||||
)
|
||||
|
||||
val newAddToPortfolioManager: NewAddToPortfolioManager?
|
||||
val newMarketsPortfolioDelegate: NewMarketsPortfolioDelegate?
|
||||
|
||||
/** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */
|
||||
private val selectedMultiWalletIdFlow = MutableStateFlow<UserWalletId?>(value = null)
|
||||
|
||||
private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel())
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = SlotNavigation()
|
||||
val bottomSheetNavigation: SlotNavigation<MarketsPortfolioRoute> = SlotNavigation()
|
||||
val addToPortfolioCallback = object : AddToPortfolioComponent.Callback {
|
||||
override fun onDismiss() = bottomSheetNavigation.dismiss()
|
||||
}
|
||||
|
||||
private val tokenActionsHandler = tokenActionsIntentsFactory.create(
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
updateTokenReceiveBSConfig = { updateBlock ->
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled.not()) {
|
||||
updateTokensState {
|
||||
it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig))
|
||||
}
|
||||
}
|
||||
},
|
||||
onHandleQuickAction = { handledAction ->
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.quickActionClick(
|
||||
actionUM = handledAction.action,
|
||||
blockchainName = handledAction.cryptoCurrencyData.status.currency.network.name,
|
||||
),
|
||||
)
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
|
||||
configureReceiveAddresses(handledAction)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
|
|
@ -132,27 +166,7 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
},
|
||||
),
|
||||
currentState = Provider { _state.value },
|
||||
tokenActionsHandler = tokenActionsIntentsFactory.create(
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
updateTokenReceiveBSConfig = { updateBlock ->
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled.not()) {
|
||||
updateTokensState {
|
||||
it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig))
|
||||
}
|
||||
}
|
||||
},
|
||||
onHandleQuickAction = { handledAction ->
|
||||
analyticsEventHandler.send(
|
||||
analyticsEventBuilder.quickActionClick(
|
||||
actionUM = handledAction.action,
|
||||
blockchainName = handledAction.cryptoCurrencyData.status.currency.network.name,
|
||||
),
|
||||
)
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
|
||||
configureReceiveAddresses(handledAction)
|
||||
}
|
||||
},
|
||||
),
|
||||
tokenActionsHandler = tokenActionsHandler,
|
||||
updateTokens = { updateBlock ->
|
||||
updateTokensState { state ->
|
||||
state.copy(tokens = updateBlock(state.tokens))
|
||||
|
|
@ -161,18 +175,50 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
// Subscribe on selected wallet flow to support actual selected wallet
|
||||
subscribeOnSelectedMultiWalletUpdates()
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
newAddToPortfolioManager = newAddToPortfolioManagerFactory
|
||||
.create(
|
||||
modelScope,
|
||||
params.token,
|
||||
params.analyticsParams,
|
||||
)
|
||||
newMarketsPortfolioDelegate = newMarketsPortfolioDelegateFactory.create(
|
||||
scope = modelScope,
|
||||
token = params.token,
|
||||
tokenActionsHandler = tokenActionsHandler,
|
||||
buttonState = newAddToPortfolioManager.state.map {
|
||||
when (it) {
|
||||
is NewAddToPortfolioManager.State.AvailableToAdd -> AddButtonState.Available
|
||||
NewAddToPortfolioManager.State.Init -> AddButtonState.Loading
|
||||
NewAddToPortfolioManager.State.NothingToAdd -> AddButtonState.Unavailable
|
||||
}
|
||||
},
|
||||
onAddClick = { bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio) },
|
||||
)
|
||||
newMarketsPortfolioDelegate.combineData()
|
||||
.onEach { _state.value = it }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
} else {
|
||||
newAddToPortfolioManager = null
|
||||
newMarketsPortfolioDelegate = null
|
||||
// Subscribe on selected wallet flow to support actual selected wallet
|
||||
subscribeOnSelectedMultiWalletUpdates()
|
||||
|
||||
subscribeOnStateUpdates()
|
||||
subscribeOnStateUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
addToPortfolioManager.setAvailableNetworks(networks)
|
||||
newAddToPortfolioManager?.setTokenNetworks(networks)
|
||||
newMarketsPortfolioDelegate?.setTokenNetworks(networks)
|
||||
}
|
||||
|
||||
fun setNoNetworksAvailable() {
|
||||
addToPortfolioManager.setAvailableNetworks(emptyList())
|
||||
newAddToPortfolioManager?.setTokenNetworks(emptyList())
|
||||
newMarketsPortfolioDelegate?.setTokenNetworks(emptyList())
|
||||
}
|
||||
|
||||
private fun subscribeOnSelectedMultiWalletUpdates() {
|
||||
|
|
@ -378,7 +424,7 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
status = quickAction.cryptoCurrencyData.status,
|
||||
userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId,
|
||||
) ?: return@launch
|
||||
bottomSheetNavigation.activate(tokenConfig)
|
||||
bottomSheetNavigation.activate(MarketsPortfolioRoute.TokenReceive(tokenConfig))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.markets.portfolio.impl.model
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed interface MarketsPortfolioRoute : Route {
|
||||
|
||||
@Serializable
|
||||
data object AddToPortfolio : MarketsPortfolioRoute
|
||||
|
||||
@Serializable
|
||||
data class TokenReceive(
|
||||
val config: TokenReceiveConfig,
|
||||
) : MarketsPortfolioRoute
|
||||
}
|
||||
|
|
@ -0,0 +1,334 @@
|
|||
package com.tangem.features.markets.portfolio.impl.model
|
||||
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
||||
import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2
|
||||
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
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.tokens.model.TokenActionsState
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioHeader
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioListItem
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.WalletHeader
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Suppress("LongParameterList")
|
||||
internal class NewMarketsPortfolioDelegate @AssistedInject constructor(
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val allAccountSupplier: MultiAccountStatusListSupplier,
|
||||
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
@Assisted private val scope: CoroutineScope,
|
||||
@Assisted private val token: TokenMarketParams,
|
||||
@Assisted private val tokenActionsHandler: TokenActionsHandler,
|
||||
@Assisted private val buttonState: Flow<AddButtonState>,
|
||||
@Assisted private val onAddClick: () -> Unit,
|
||||
) {
|
||||
|
||||
private val currencyRawId: CryptoCurrency.RawID = token.id
|
||||
private var expandedHolder: MutableStateFlow<Set<Pair<UserWalletId, CryptoCurrency.ID>>>? = null
|
||||
|
||||
private val settingsFlow: Flow<SettingsBox> = combine(
|
||||
flow = getSelectedAppCurrencyUseCase.invokeOrDefault(),
|
||||
flow2 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
|
||||
flow3 = isAccountsModeEnabledUseCase(),
|
||||
transform = ::SettingsBox,
|
||||
).shareIn(
|
||||
replay = 1,
|
||||
started = SharingStarted.Eagerly,
|
||||
scope = scope,
|
||||
).distinctUntilChanged()
|
||||
|
||||
private val availableNetworks = MutableSharedFlow<List<TokenMarketInfo.Network>>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
|
||||
availableNetworks.tryEmit(networks)
|
||||
}
|
||||
|
||||
fun combineData(): Flow<MyPortfolioUM> {
|
||||
return availableNetworks.transformLatest { availableNetworks ->
|
||||
when {
|
||||
availableNetworks.isEmpty() -> emit(MyPortfolioUM.Unavailable)
|
||||
else -> emitAll(onAvailableNetworksFlow().distinctUntilChanged())
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
private fun onAvailableNetworksFlow(): Flow<MyPortfolioUM> =
|
||||
portfolioWithThisCurrencyFLow().transformLatest { portfolioWithCurrency ->
|
||||
when (portfolioWithCurrency.flattenAddedCurrency.isEmpty()) {
|
||||
false -> emitAll(contentFlow(portfolioWithCurrency).distinctUntilChanged())
|
||||
true -> when (portfolioWithCurrency.hasMultiWallets) {
|
||||
true -> emitAll(addFirstTokenFlow())
|
||||
false -> emit(MyPortfolioUM.UnavailableForWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addFirstTokenFlow(): Flow<MyPortfolioUM> = buttonState.map {
|
||||
when (it) {
|
||||
AddButtonState.Loading -> MyPortfolioUM.Loading
|
||||
AddButtonState.Available -> MyPortfolioUM.AddFirstToken(
|
||||
onAddClick = onAddClick,
|
||||
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
|
||||
)
|
||||
AddButtonState.Unavailable -> MyPortfolioUM.Unavailable
|
||||
}
|
||||
}
|
||||
|
||||
private fun contentFlow(portfolio: PortfoliosWithThisCurrency): Flow<MyPortfolioUM.Content> {
|
||||
fun Portfolio.actionsFoAccountCurrencies(): List<Flow<Pair<CryptoCurrency, TokenActionsState>>> =
|
||||
accountsWithAdded.map { account ->
|
||||
fun CryptoCurrencyStatus.actionsFlow() = getCryptoCurrencyActionsUseCase(
|
||||
accountId = account.accountStatus.account.accountId,
|
||||
currency = this.currency,
|
||||
).map { actionsState -> actionsState.cryptoCurrencyStatus.currency to actionsState }
|
||||
account.addedCurrency.map { it.actionsFlow() }
|
||||
}.flatten()
|
||||
|
||||
val allAddedTokenActions =
|
||||
portfolio.portfolios.map { portfolio -> portfolio.actionsFoAccountCurrencies() }.flatten()
|
||||
|
||||
return combine(
|
||||
flow = combine(allAddedTokenActions) { it.toMap() }.distinctUntilChanged(),
|
||||
flow2 = buttonState.distinctUntilChanged(),
|
||||
flow3 = getExpandedHolder(portfolio),
|
||||
flow4 = settingsFlow.distinctUntilChanged(),
|
||||
transform = { actions, addButtonState, expanded, settings ->
|
||||
buildContentState(
|
||||
portfolio = portfolio,
|
||||
allActions = actions,
|
||||
addButtonState = addButtonState,
|
||||
expanded = expanded,
|
||||
settings = settings,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getExpandedHolder(
|
||||
portfolio: PortfoliosWithThisCurrency,
|
||||
): StateFlow<Set<Pair<UserWalletId, CryptoCurrency.ID>>> {
|
||||
val expandedHolder = this.expandedHolder
|
||||
if (expandedHolder != null) return expandedHolder
|
||||
val allAddedCurrency = portfolio.flattenAddedCurrency
|
||||
val shouldForceExpand = allAddedCurrency.size == 1 &&
|
||||
allAddedCurrency.first().value.amount?.isZero() == true
|
||||
|
||||
val initValue = when {
|
||||
shouldForceExpand -> {
|
||||
val currency = allAddedCurrency.first()
|
||||
// find userWallet than have this single added token
|
||||
portfolio.portfolios
|
||||
.find { it.accountsWithAdded.find { account -> account.addedCurrency.isNotEmpty() } != null }
|
||||
?.userWallet
|
||||
?.let { setOf(it.walletId to currency.currency.id) }
|
||||
?: setOf()
|
||||
}
|
||||
else -> setOf()
|
||||
}
|
||||
return MutableStateFlow(initValue)
|
||||
.also { this.expandedHolder = it }
|
||||
}
|
||||
|
||||
private fun portfolioWithThisCurrencyFLow(): Flow<PortfoliosWithThisCurrency> =
|
||||
allAccountSupplier().map { list -> list.map { it.addedAccountsFlow() } }.flatMapLatest { flows ->
|
||||
combine(flows) {
|
||||
PortfoliosWithThisCurrency(
|
||||
currencyRawId = currencyRawId,
|
||||
portfolios = it.toList(),
|
||||
)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
|
||||
private fun AccountStatusList.addedAccountsFlow(): Flow<Portfolio> =
|
||||
getUserWalletUseCase.invokeFlow(this.userWalletId).mapNotNull { it.getOrNull() }.map { wallet ->
|
||||
Portfolio(
|
||||
userWallet = wallet,
|
||||
accountStatusList = this,
|
||||
accountsWithAdded = this.filterByRawID(),
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
|
||||
private fun AccountStatusList.filterByRawID(): List<AccountWithAdded> {
|
||||
fun AccountStatus.filterByRawID(): List<CryptoCurrencyStatus> = when (this) {
|
||||
is AccountStatus.CryptoPortfolio -> this.tokenList.flattenCurrencies()
|
||||
.filter { status -> status.currency.id.rawCurrencyId == currencyRawId }
|
||||
}
|
||||
return accountStatuses.map { accountStatus ->
|
||||
AccountWithAdded(
|
||||
accountStatus = accountStatus,
|
||||
addedCurrency = accountStatus.filterByRawID(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildContentState(
|
||||
portfolio: PortfoliosWithThisCurrency,
|
||||
allActions: Map<CryptoCurrency, TokenActionsState>,
|
||||
addButtonState: AddButtonState,
|
||||
expanded: Set<Pair<UserWalletId, CryptoCurrency.ID>>,
|
||||
settings: SettingsBox,
|
||||
): MyPortfolioUM.Content {
|
||||
val appCurrency = settings.appCurrency
|
||||
val isBalanceHidden = settings.isBalanceHidden
|
||||
val isAccountMode = settings.isAccountMode
|
||||
val uiItems: MutableList<PortfolioListItem> = mutableListOf()
|
||||
|
||||
fun toggleQuickActions(key: Pair<UserWalletId, CryptoCurrency.ID>) = expandedHolder?.update { expanded ->
|
||||
val isExpand = expanded.contains(key)
|
||||
if (isExpand) expanded.minus(key) else expanded.plus(key)
|
||||
}
|
||||
|
||||
val tokenUMConverter = PortfolioTokenUMConverter(
|
||||
appCurrency = appCurrency,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onTokenItemClick = { },
|
||||
tokenActionsHandler = tokenActionsHandler,
|
||||
)
|
||||
|
||||
portfolio.portfolios.forEach { portfolioItem ->
|
||||
if (portfolioItem.flattenAddedCurrency.isEmpty()) return@forEach
|
||||
val userWallet = portfolioItem.userWallet
|
||||
if (isAccountMode) {
|
||||
uiItems.add(portfolioItem.userWallet.toWalletHeader())
|
||||
} else {
|
||||
uiItems.add(portfolioItem.userWallet.toWalletPortfolioHeader())
|
||||
}
|
||||
|
||||
portfolioItem.accountsWithAdded.forEach { accountWithAdded ->
|
||||
if (accountWithAdded.addedCurrency.isEmpty()) return@forEach
|
||||
if (isAccountMode) {
|
||||
val account = accountWithAdded.accountStatus.account
|
||||
uiItems.add(account.toAccountPortfolioHeader())
|
||||
}
|
||||
|
||||
accountWithAdded.addedCurrency.forEach { currencyStatus ->
|
||||
val actions = allActions[currencyStatus.currency]?.states
|
||||
?: emptyList()
|
||||
val value = PortfolioData.CryptoCurrencyData(
|
||||
userWallet = userWallet,
|
||||
status = currencyStatus,
|
||||
actions = actions,
|
||||
)
|
||||
val expandedKey = portfolioItem.userWallet.walletId to currencyStatus.currency.id
|
||||
val isExpand = expanded.contains(expandedKey)
|
||||
|
||||
val tokenItem = tokenUMConverter.convertV2(
|
||||
onTokenItemClick = { wallet, status ->
|
||||
toggleQuickActions(wallet.walletId to status.currency.id)
|
||||
},
|
||||
value = value,
|
||||
isQuickActionsShown = isExpand,
|
||||
)
|
||||
uiItems.add(tokenItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MyPortfolioUM.Content(
|
||||
items = uiItems.toImmutableList(),
|
||||
buttonState = addButtonState,
|
||||
onAddClick = onAddClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Account.toAccountPortfolioHeader(): PortfolioHeader = PortfolioHeader(
|
||||
id = this.accountId.value,
|
||||
state = AccountTitleUM.Account(
|
||||
prefixText = TextReference.EMPTY,
|
||||
name = this.accountName.toUM().value,
|
||||
icon = when (this) {
|
||||
is Account.CryptoPortfolio -> this.icon.toUM()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
private fun UserWallet.toWalletPortfolioHeader(): PortfolioHeader = PortfolioHeader(
|
||||
id = this.walletId.stringValue,
|
||||
state = AccountTitleUM.Text(
|
||||
title = stringReference(this.name),
|
||||
),
|
||||
)
|
||||
|
||||
private fun UserWallet.toWalletHeader(): WalletHeader = WalletHeader(
|
||||
id = this.walletId.stringValue,
|
||||
name = stringReference(this.name),
|
||||
)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
scope: CoroutineScope,
|
||||
token: TokenMarketParams,
|
||||
tokenActionsHandler: TokenActionsHandler,
|
||||
buttonState: Flow<AddButtonState>,
|
||||
onAddClick: () -> Unit,
|
||||
): NewMarketsPortfolioDelegate
|
||||
}
|
||||
}
|
||||
|
||||
private data class PortfoliosWithThisCurrency(
|
||||
val currencyRawId: CryptoCurrency.RawID,
|
||||
val portfolios: List<Portfolio>,
|
||||
) {
|
||||
|
||||
val hasMultiWallets: Boolean = portfolios.any { it.userWallet.isMultiCurrency }
|
||||
|
||||
val flattenAddedCurrency: List<CryptoCurrencyStatus> =
|
||||
portfolios.map { portfolio -> portfolio.flattenAddedCurrency }.flatten()
|
||||
}
|
||||
|
||||
private data class Portfolio(
|
||||
val userWallet: UserWallet,
|
||||
val accountStatusList: AccountStatusList,
|
||||
val accountsWithAdded: List<AccountWithAdded>,
|
||||
) {
|
||||
val flattenAddedCurrency: List<CryptoCurrencyStatus> =
|
||||
accountsWithAdded.map { it.addedCurrency }.flatten()
|
||||
}
|
||||
|
||||
private data class AccountWithAdded(
|
||||
val addedCurrency: List<CryptoCurrencyStatus>,
|
||||
val accountStatus: AccountStatus,
|
||||
)
|
||||
|
||||
private data class SettingsBox(
|
||||
val appCurrency: AppCurrency,
|
||||
val isBalanceHidden: Boolean,
|
||||
val isAccountMode: Boolean,
|
||||
)
|
||||
|
|
@ -27,6 +27,24 @@ internal class PortfolioTokenUMConverter(
|
|||
private val tokenActionsHandler: TokenActionsHandler,
|
||||
) : Converter<PortfolioData.CryptoCurrencyData, PortfolioTokenUM> {
|
||||
|
||||
fun convertV2(
|
||||
value: PortfolioData.CryptoCurrencyData,
|
||||
isQuickActionsShown: Boolean,
|
||||
onTokenItemClick: (UserWallet, CryptoCurrencyStatus) -> Unit,
|
||||
): PortfolioTokenUM {
|
||||
val tokenItemStateConverter = TokenItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
onItemClick = { _, status -> onTokenItemClick(value.userWallet, status) },
|
||||
)
|
||||
return PortfolioTokenUM(
|
||||
tokenItemState = tokenItemStateConverter.convert(value = value.status),
|
||||
walletId = value.userWallet.walletId,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
isQuickActionsShown = isQuickActionsShown,
|
||||
quickActions = quickActions(cryptoData = value, tokenActionsHandler = tokenActionsHandler),
|
||||
)
|
||||
}
|
||||
|
||||
override fun convert(value: PortfolioData.CryptoCurrencyData): PortfolioTokenUM {
|
||||
val tokenItemStateConverter = TokenItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
|
|
@ -84,7 +102,7 @@ internal class PortfolioTokenUMConverter(
|
|||
)
|
||||
}
|
||||
|
||||
private fun toQuickActions(actions: List<TokenActionsState.ActionState>) = buildList {
|
||||
fun toQuickActions(actions: List<TokenActionsState.ActionState>) = buildList {
|
||||
actions.forEach { action ->
|
||||
if (action.unavailabilityReason == ScenarioUnavailabilityReason.None) {
|
||||
when (action) {
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
router.push(
|
||||
AppRoute.Staking(
|
||||
userWalletId = cryptoCurrencyData.userWallet.walletId,
|
||||
cryptoCurrencyId = cryptoCurrencyData.status.currency.id,
|
||||
cryptoCurrency = cryptoCurrencyData.status.currency,
|
||||
yieldId = yield.id,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,29 +10,43 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.common.ui.account.AccountTitle
|
||||
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SmallButtonShimmer
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
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.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.*
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState
|
||||
|
||||
@Composable
|
||||
internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) {
|
||||
if (state is MyPortfolioUM.Content) {
|
||||
val contentModifier = Modifier.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing20,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing32,
|
||||
)
|
||||
PortfolioList(state, contentModifier)
|
||||
return
|
||||
}
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
contentHorizontalPadding = TangemTheme.dimens.spacing0,
|
||||
|
|
@ -55,6 +69,7 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) {
|
|||
MyPortfolioUM.Loading -> LoadingPlaceholder(modifier = contentModifier)
|
||||
MyPortfolioUM.Unavailable -> UnavailableAsset(modifier = contentModifier)
|
||||
MyPortfolioUM.UnavailableForWallet -> UnavailableAssetForWallet(modifier = contentModifier)
|
||||
is MyPortfolioUM.Content -> PortfolioList(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,6 +134,7 @@ private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier
|
|||
state.tokens.fastForEachIndexed { index, token ->
|
||||
key(token.tokenItemState.id) {
|
||||
PortfolioItem(
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.action),
|
||||
state = token,
|
||||
lastInList = index == state.tokens.size - 1,
|
||||
)
|
||||
|
|
@ -129,6 +145,111 @@ private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier
|
|||
TokenReceiveBottomSheet(config = state.tokenReceiveBSConfig)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PortfolioList(state: MyPortfolioUM.Content, modifier: Modifier = Modifier) {
|
||||
Column(modifier) {
|
||||
key("PortfolioListHeader") {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResourceSafe(R.string.markets_common_my_portfolio),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
AddButton(state = state.buttonState, onClick = state.onAddClick)
|
||||
}
|
||||
}
|
||||
|
||||
state.items.fastForEachIndexed { index, item ->
|
||||
val previousItem = state.items.getOrNull(index.dec())
|
||||
val nextItem = state.items.getOrNull(index.inc())
|
||||
val itemModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.getOffsetModifier(item, previousItem)
|
||||
.getBackgroundModifier(item, previousItem, nextItem)
|
||||
|
||||
key(item.id) {
|
||||
PortfolioItem(
|
||||
item = item,
|
||||
modifier = itemModifier,
|
||||
lastInList = index == state.items.size - 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Modifier.getBackgroundModifier(
|
||||
item: PortfolioListItem,
|
||||
previousItem: PortfolioListItem?,
|
||||
nextItem: PortfolioListItem?,
|
||||
): Modifier {
|
||||
val color = TangemTheme.colors.background.action
|
||||
val radius = 14.dp
|
||||
val topRound = RoundedCornerShape(topStart = radius, topEnd = radius)
|
||||
val bottomRound = RoundedCornerShape(bottomStart = radius, bottomEnd = radius)
|
||||
val allRound = RoundedCornerShape(size = radius)
|
||||
val backgroundModifier = when (item) {
|
||||
is WalletHeader -> this
|
||||
is PortfolioHeader -> this
|
||||
.clip(topRound)
|
||||
.background(color = color)
|
||||
is PortfolioTokenUM -> when {
|
||||
previousItem is PortfolioHeader && nextItem !is PortfolioTokenUM -> this
|
||||
.clip(bottomRound)
|
||||
.background(color = color)
|
||||
previousItem is WalletHeader && nextItem !is PortfolioTokenUM -> this
|
||||
.clip(allRound)
|
||||
.background(color = color)
|
||||
previousItem is PortfolioTokenUM && nextItem !is PortfolioTokenUM -> this
|
||||
.clip(bottomRound)
|
||||
.background(color = color)
|
||||
else -> this.background(color = color)
|
||||
}
|
||||
}
|
||||
return backgroundModifier
|
||||
}
|
||||
|
||||
private fun Modifier.getOffsetModifier(item: PortfolioListItem, previousItem: PortfolioListItem?): Modifier = when {
|
||||
item is WalletHeader -> this.padding(top = 20.dp, start = 4.dp, end = 4.dp)
|
||||
item is PortfolioHeader && previousItem is PortfolioTokenUM -> this.padding(top = 12.dp)
|
||||
item is PortfolioHeader && previousItem == null -> this.padding(top = 20.dp)
|
||||
previousItem is WalletHeader -> this.padding(top = 12.dp)
|
||||
previousItem is PortfolioTokenUM -> this
|
||||
else -> this
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PortfolioItem(item: PortfolioListItem, lastInList: Boolean, modifier: Modifier = Modifier) {
|
||||
when (item) {
|
||||
is PortfolioHeader -> AccountTitle(
|
||||
modifier = modifier.padding(
|
||||
start = 12.dp,
|
||||
top = 12.dp,
|
||||
bottom = 8.dp,
|
||||
),
|
||||
accountTitleUM = item.state,
|
||||
textStyle = TangemTheme.typography.caption1,
|
||||
textColor = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
is PortfolioTokenUM -> PortfolioItem(
|
||||
state = item,
|
||||
modifier = modifier,
|
||||
lastInList = lastInList,
|
||||
)
|
||||
is WalletHeader -> Text(
|
||||
modifier = modifier,
|
||||
text = item.name.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UnavailableAsset(modifier: Modifier = Modifier) {
|
||||
UnavailableContent(
|
||||
|
|
@ -199,8 +320,7 @@ private fun Preview(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state
|
|||
TangemThemePreview {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(TangemTheme.dimens.spacing8),
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
MyPortfolio(state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie
|
|||
TokenItem(
|
||||
state = tokenItemState,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.action),
|
||||
itemPaddingValues = PaddingValues(
|
||||
start = TangemTheme.dimens.spacing10,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
|
|
@ -54,7 +53,6 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie
|
|||
|
||||
PortfolioQuickActions(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.action)
|
||||
.padding(
|
||||
bottom = if (lastInList) {
|
||||
TangemTheme.dimens.spacing12
|
||||
|
|
@ -82,6 +80,7 @@ private fun Preview(@PreviewParameter(PortfolioTokenUMProvider::class) tokenUM:
|
|||
}
|
||||
|
||||
PortfolioItem(
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.action),
|
||||
state = tokenUM.copy(
|
||||
tokenItemState = when (tokenUM.tokenItemState) {
|
||||
is TokenItemState.Content -> tokenUM.tokenItemState.copy(onItemClick = { onItemClick() })
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
package com.tangem.features.markets.portfolio.impl.ui.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.account.AccountIconPreviewData
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.*
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.util.UUID
|
||||
|
||||
internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfolioUM> {
|
||||
|
||||
|
|
@ -40,35 +44,107 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfol
|
|||
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.Content(
|
||||
items = persistentListOf(
|
||||
walletPortfolioHeader,
|
||||
accountToken,
|
||||
accountToken,
|
||||
),
|
||||
buttonState = Tokens.AddButtonState.Available,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.Content(
|
||||
items = persistentListOf(
|
||||
walletHeader,
|
||||
accountHeader,
|
||||
accountToken,
|
||||
accountToken,
|
||||
),
|
||||
buttonState = Tokens.AddButtonState.Available,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.Content(
|
||||
items = persistentListOf(
|
||||
walletHeader,
|
||||
accountHeader,
|
||||
accountToken.copy(isQuickActionsShown = true),
|
||||
accountToken,
|
||||
),
|
||||
buttonState = Tokens.AddButtonState.Available,
|
||||
onAddClick = {},
|
||||
),
|
||||
MyPortfolioUM.Loading,
|
||||
MyPortfolioUM.Unavailable,
|
||||
MyPortfolioUM.UnavailableForWallet,
|
||||
)
|
||||
|
||||
val sampleToken = PortfolioTokenUM(
|
||||
tokenItemState = TokenItemState.Content(
|
||||
id = "",
|
||||
iconState = CurrencyIconState.Locked,
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "My wallet")),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "486,65 \$"),
|
||||
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "733,71097 MATIC"),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(
|
||||
value = stringReference(value = "XRP Ledger token"),
|
||||
val walletHeader
|
||||
get() = WalletHeader(
|
||||
id = UUID.randomUUID().toString(),
|
||||
name = stringReference("Wallet 1"),
|
||||
)
|
||||
|
||||
val walletPortfolioHeader
|
||||
get() = PortfolioHeader(
|
||||
state = AccountTitleUM.Text(title = stringReference("Wallet 1")),
|
||||
id = UUID.randomUUID().toString(),
|
||||
)
|
||||
|
||||
val accountHeader
|
||||
get() = PortfolioHeader(
|
||||
state = AccountTitleUM.Account(
|
||||
icon = AccountIconPreviewData.randomAccountIcon(),
|
||||
name = stringReference("Main Account"),
|
||||
prefixText = TextReference.EMPTY,
|
||||
),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
),
|
||||
isQuickActionsShown = false,
|
||||
quickActions = PortfolioTokenUM.QuickActions(
|
||||
actions = persistentListOf(
|
||||
QuickActionUM.Buy,
|
||||
QuickActionUM.Exchange(showBadge = true),
|
||||
QuickActionUM.Receive,
|
||||
id = UUID.randomUUID().toString(),
|
||||
)
|
||||
val coinIconState
|
||||
get() = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = com.tangem.core.ui.R.drawable.img_polygon_22,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
)
|
||||
val accountToken
|
||||
get() = sampleToken.copy(
|
||||
tokenItemState = TokenItemState.Content(
|
||||
id = UUID.randomUUID().toString(),
|
||||
iconState = coinIconState,
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
|
||||
fiatAmountState = FiatAmountState.Content(text = "321 $"),
|
||||
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "5,412 MATIC"),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(value = "Token")),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
),
|
||||
onQuickActionClick = {},
|
||||
onQuickActionLongClick = {},
|
||||
),
|
||||
isBalanceHidden = false,
|
||||
walletId = UserWalletId(""),
|
||||
)
|
||||
)
|
||||
|
||||
val sampleToken
|
||||
get() = PortfolioTokenUM(
|
||||
tokenItemState = TokenItemState.Content(
|
||||
id = UUID.randomUUID().toString(),
|
||||
iconState = CurrencyIconState.Locked,
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "My wallet")),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "486,65 \$"),
|
||||
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "733,71097 MATIC"),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(
|
||||
value = stringReference(value = "XRP Ledger token"),
|
||||
),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
),
|
||||
isQuickActionsShown = false,
|
||||
quickActions = PortfolioTokenUM.QuickActions(
|
||||
actions = persistentListOf(
|
||||
QuickActionUM.Buy,
|
||||
QuickActionUM.Exchange(showBadge = true),
|
||||
QuickActionUM.Receive,
|
||||
),
|
||||
onQuickActionClick = {},
|
||||
onQuickActionLongClick = {},
|
||||
),
|
||||
isBalanceHidden = false,
|
||||
walletId = UserWalletId(""),
|
||||
)
|
||||
}
|
||||
|
|
@ -24,6 +24,15 @@ internal sealed class MyPortfolioUM {
|
|||
}
|
||||
}
|
||||
|
||||
data class Content(
|
||||
val items: ImmutableList<PortfolioListItem>,
|
||||
val buttonState: Tokens.AddButtonState,
|
||||
val onAddClick: () -> Unit,
|
||||
) : MyPortfolioUM() {
|
||||
|
||||
override val addToPortfolioBSConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty
|
||||
}
|
||||
|
||||
data class AddFirstToken(
|
||||
override val addToPortfolioBSConfig: TangemBottomSheetConfig,
|
||||
val onAddClick: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,33 @@
|
|||
package com.tangem.features.markets.portfolio.impl.ui.state
|
||||
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal sealed interface PortfolioListItem {
|
||||
val id: String
|
||||
}
|
||||
|
||||
internal data class WalletHeader(
|
||||
override val id: String,
|
||||
val name: TextReference,
|
||||
) : PortfolioListItem
|
||||
|
||||
internal data class PortfolioHeader(
|
||||
override val id: String,
|
||||
val state: AccountTitleUM,
|
||||
) : PortfolioListItem
|
||||
|
||||
internal data class PortfolioTokenUM(
|
||||
val tokenItemState: TokenItemState,
|
||||
val walletId: UserWalletId,
|
||||
val isBalanceHidden: Boolean,
|
||||
val isQuickActionsShown: Boolean,
|
||||
val quickActions: QuickActions,
|
||||
) {
|
||||
) : PortfolioListItem {
|
||||
override val id: String = tokenItemState.id
|
||||
|
||||
data class QuickActions(
|
||||
val actions: ImmutableList<QuickActionUM>,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ android {
|
|||
|
||||
dependencies {
|
||||
/** Api */
|
||||
implementation(projects.features.account.api)
|
||||
implementation(projects.features.nft.api)
|
||||
implementation(projects.features.tokenRecieve.api)
|
||||
|
||||
|
|
@ -28,6 +29,8 @@ dependencies {
|
|||
implementation(projects.core.datasource)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.models)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,20 @@
|
|||
package com.tangem.features.nft.collections.entity
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal sealed interface NFTCollectionItem {
|
||||
val id: String
|
||||
}
|
||||
|
||||
internal data class NFTCollectionPortfolioUM(
|
||||
override val id: String,
|
||||
val title: AccountTitleUM,
|
||||
) : NFTCollectionItem
|
||||
|
||||
internal data class NFTCollectionUM(
|
||||
val id: String,
|
||||
override val id: String,
|
||||
val name: String,
|
||||
@DrawableRes val networkIconId: Int,
|
||||
val logoUrl: String?,
|
||||
|
|
@ -12,4 +22,4 @@ internal data class NFTCollectionUM(
|
|||
val assets: NFTCollectionAssetsListUM,
|
||||
val isExpanded: Boolean,
|
||||
val onExpandClick: () -> Unit,
|
||||
)
|
||||
) : NFTCollectionItem
|
||||
|
|
@ -23,7 +23,7 @@ internal sealed class NFTCollectionsUM {
|
|||
|
||||
data class Content(
|
||||
val search: SearchBarUM,
|
||||
val collections: ImmutableList<NFTCollectionUM>,
|
||||
val collections: ImmutableList<NFTCollectionItem>,
|
||||
val warnings: ImmutableList<NFTCollectionsWarningUM>,
|
||||
val onReceiveClick: () -> Unit,
|
||||
) : NFTCollectionsUM()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.nft.collections.entity.transformer
|
||||
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionUM
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
|
@ -21,7 +22,7 @@ internal class ChangeCollectionExpandedStateTransformer(
|
|||
is NFTCollectionsUM.Content -> prevState.content.copy(
|
||||
collections = prevState.content.collections.map {
|
||||
val collectionId = collection.collectionIdProvider()
|
||||
if (it.id == collectionId) {
|
||||
if (it.id == collectionId && it is NFTCollectionUM) {
|
||||
if (!it.isExpanded) {
|
||||
onFirstExpanded()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package com.tangem.features.nft.collections.entity.transformer
|
||||
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.nft.models.*
|
||||
import com.tangem.features.nft.collections.entity.*
|
||||
import com.tangem.features.nft.impl.R
|
||||
|
|
@ -16,6 +19,8 @@ import kotlinx.collections.immutable.toPersistentList
|
|||
@Suppress("LongParameterList")
|
||||
internal class UpdateDataStateTransformer(
|
||||
private val nftCollections: List<NFTCollections>,
|
||||
private val walletNFTCollections: WalletNFTCollections? = null,
|
||||
private val isAccountMode: Boolean = false,
|
||||
private val onReceiveClick: () -> Unit,
|
||||
private val onRetryClick: () -> Unit,
|
||||
private val onExpandCollectionClick: (NFTCollection) -> Unit,
|
||||
|
|
@ -25,7 +30,9 @@ internal class UpdateDataStateTransformer(
|
|||
private val collectionIdProvider: NFTCollection.() -> String,
|
||||
) : Transformer<NFTCollectionsStateUM> {
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM {
|
||||
val nftCollections = walletNFTCollections?.flattenCollections ?: this.nftCollections
|
||||
val hasQuery = !(prevState.content as? NFTCollectionsUM.Content)?.search?.query.isNullOrEmpty()
|
||||
val content = when {
|
||||
!hasQuery && nftCollections.allCollectionsFailed() ->
|
||||
|
|
@ -66,16 +73,45 @@ internal class UpdateDataStateTransformer(
|
|||
} else {
|
||||
initialSearchBarFactory()
|
||||
},
|
||||
collections = nftCollections
|
||||
collections = walletNFTCollections
|
||||
?.let { createCollections(it) }
|
||||
?: createNFTsUM(nftCollections).toPersistentList(),
|
||||
warnings = transformNotifications(),
|
||||
onReceiveClick = onReceiveClick,
|
||||
)
|
||||
|
||||
private fun NFTCollectionsStateUM.createCollections(walletNFTCollections: WalletNFTCollections) =
|
||||
if (isAccountMode) {
|
||||
val result = mutableListOf<NFTCollectionItem>()
|
||||
walletNFTCollections.collections.forEach { (account, nfts) ->
|
||||
if (nfts.isEmpty()) return@forEach
|
||||
result.add(account.toAccountPortfolioUM())
|
||||
result.addAll(createNFTsUM(nfts))
|
||||
}
|
||||
result.toPersistentList()
|
||||
} else {
|
||||
val mainAccountCollection = walletNFTCollections.collections.values.firstOrNull() ?: listOf()
|
||||
createNFTsUM(mainAccountCollection).toPersistentList()
|
||||
}
|
||||
|
||||
private fun Account.toAccountPortfolioUM(): NFTCollectionPortfolioUM = NFTCollectionPortfolioUM(
|
||||
id = this.accountId.value,
|
||||
title = AccountTitleUM.Account(
|
||||
prefixText = TextReference.EMPTY,
|
||||
name = this.accountName.toUM().value,
|
||||
icon = when (this) {
|
||||
is Account.CryptoPortfolio -> this.icon.toUM()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
private fun NFTCollectionsStateUM.createNFTsUM(nftCollections: List<NFTCollections>): Sequence<NFTCollectionUM> =
|
||||
nftCollections
|
||||
.map { it.content }
|
||||
.asSequence()
|
||||
.filterIsInstance<NFTCollections.Content.Collections>()
|
||||
.map { it.collections.orEmpty().transform(this) }
|
||||
.flatten()
|
||||
.toPersistentList(),
|
||||
warnings = transformNotifications(),
|
||||
onReceiveClick = onReceiveClick,
|
||||
)
|
||||
|
||||
private fun List<NFTCollection>.transform(state: NFTCollectionsStateUM): ImmutableList<NFTCollectionUM> = map {
|
||||
NFTCollectionUM(
|
||||
|
|
@ -97,6 +133,8 @@ internal class UpdateDataStateTransformer(
|
|||
}.toPersistentList()
|
||||
|
||||
private fun transformNotifications(): ImmutableList<NFTCollectionsWarningUM> = buildList {
|
||||
val nftCollections = walletNFTCollections?.flattenCollections
|
||||
?: this@UpdateDataStateTransformer.nftCollections
|
||||
if (nftCollections.anyCollectionFailed()) {
|
||||
add(
|
||||
NFTCollectionsWarningUM(
|
||||
|
|
@ -152,6 +190,7 @@ internal class UpdateDataStateTransformer(
|
|||
private fun NFTCollection.isExpanded(state: NFTCollectionsStateUM): Boolean =
|
||||
(state.content as? NFTCollectionsUM.Content)
|
||||
?.collections
|
||||
?.filterIsInstance<NFTCollectionUM>()
|
||||
?.firstOrNull { it.id == this.collectionIdProvider() }
|
||||
?.isExpanded
|
||||
?: false
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi
|
|||
import com.tangem.core.ui.components.fields.InputManager
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase
|
||||
import com.tangem.domain.nft.GetNFTCollectionsUseCase
|
||||
import com.tangem.domain.nft.RefreshAllNFTUseCase
|
||||
|
|
@ -31,6 +33,8 @@ internal class NFTCollectionsModel @Inject constructor(
|
|||
private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase,
|
||||
private val fetchNFTCollectionAssetsUseCase: FetchNFTCollectionAssetsUseCase,
|
||||
private val refreshAllNFTUseCase: RefreshAllNFTUseCase,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -62,7 +66,11 @@ internal class NFTCollectionsModel @Inject constructor(
|
|||
}
|
||||
|
||||
init {
|
||||
subscribeToNFTCollections()
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
subscribeToNFTCollectionsNew()
|
||||
} else {
|
||||
subscribeToNFTCollections()
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToNFTCollections() {
|
||||
|
|
@ -91,6 +99,38 @@ internal class NFTCollectionsModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeToNFTCollectionsNew() {
|
||||
combine(
|
||||
flow = getNFTCollectionsUseCase.invokeForAccounts(params.userWalletId),
|
||||
flow2 = searchManager.query.distinctUntilChanged(),
|
||||
flow3 = isAccountsModeEnabledUseCase(),
|
||||
) { nftCollections, query, isAccountMode ->
|
||||
val filteredNFTs = nftCollections.collections
|
||||
.mapValues { (_, nfts) -> nfts.filter(query) }
|
||||
|
||||
_state.update {
|
||||
UpdateDataStateTransformer(
|
||||
nftCollections = listOf(),
|
||||
isAccountMode = isAccountMode,
|
||||
walletNFTCollections = nftCollections.copy(collections = filteredNFTs),
|
||||
onReceiveClick = {
|
||||
params.onReceiveClick()
|
||||
},
|
||||
onRetryClick = ::onRefresh,
|
||||
onExpandCollectionClick = ::onExpandCollectionClick,
|
||||
onRetryAssetsClick = ::onRetryAssetsClick,
|
||||
onAssetClick = { asset, collection ->
|
||||
params.onAssetClick(asset, collection)
|
||||
},
|
||||
initialSearchBarFactory = ::getInitialSearchBar,
|
||||
collectionIdProvider = collectionIdProvider,
|
||||
).transform(it)
|
||||
}
|
||||
}
|
||||
.onStart { onRefresh() }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun List<NFTCollections>.filter(query: String): List<NFTCollections> = map {
|
||||
it.copy(
|
||||
content = when (val content = it.content) {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue