Updated on 2026-08-14
This commit is contained in:
commit
5ee5b4f61b
563 changed files with 13917 additions and 4692 deletions
|
|
@ -2,15 +2,15 @@ package com.tangem.features.account
|
|||
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* How to use see [PortfolioSelectorComponent]
|
||||
*/
|
||||
interface PortfolioFetcher {
|
||||
|
||||
val data: Flow<Data>
|
||||
|
|
@ -25,10 +25,11 @@ interface PortfolioFetcher {
|
|||
)
|
||||
|
||||
data class PortfolioBalance(
|
||||
val walletBalance: Lce<TokenListError, TotalFiatBalance>,
|
||||
val userWallet: UserWallet,
|
||||
val accountsBalance: AccountStatusList,
|
||||
) {
|
||||
val userWallet: UserWallet get() = accountsBalance.userWallet
|
||||
val walletBalance get() = accountsBalance.totalFiatBalance
|
||||
val userWalletId: UserWalletId get() = userWallet.walletId
|
||||
}
|
||||
|
||||
sealed interface Mode {
|
||||
|
|
@ -36,6 +37,9 @@ interface PortfolioFetcher {
|
|||
data class Wallet(val walletId: UserWalletId) : Mode
|
||||
}
|
||||
|
||||
/**
|
||||
* @param[mode] supports runtime change [PortfolioFetcher.updateMode]
|
||||
*/
|
||||
interface Factory {
|
||||
fun create(mode: Mode, scope: CoroutineScope): PortfolioFetcher
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,27 +8,55 @@ import com.tangem.domain.models.account.AccountId
|
|||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* How to use
|
||||
* 1) Create and keep instance of [PortfolioFetcher] and [PortfolioSelectorController] in your Feature Model
|
||||
* 2) Provide them via [Params]
|
||||
* 3) Now you have a bridge between your Feature and PortfolioSelector
|
||||
* 4) Initial state is unselected. Select yourself [PortfolioSelectorController.selectAccount]
|
||||
* or offer users to select
|
||||
*
|
||||
* 5) Listen [PortfolioSelectorController.selectedAccount] or [PortfolioSelectorController.selectedAccountWithData]
|
||||
*
|
||||
* Note:
|
||||
* - Supports [PortfolioSelectorComponent.BottomSheet] and [PortfolioSelectorComponent.Content] modes
|
||||
*/
|
||||
interface PortfolioSelectorComponent : ComposableBottomSheetComponent, ComposableContentComponent {
|
||||
|
||||
val title: StateFlow<TextReference>
|
||||
|
||||
data class Params(
|
||||
val onDismiss: () -> Unit,
|
||||
val portfolioFetcher: PortfolioFetcher,
|
||||
val controller: PortfolioSelectorController,
|
||||
val bsCallback: BottomSheetCallback? = null,
|
||||
)
|
||||
|
||||
interface BottomSheetCallback {
|
||||
val onDismiss: () -> Unit
|
||||
val onBack: () -> Unit
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, PortfolioSelectorComponent>
|
||||
}
|
||||
|
||||
/**
|
||||
* How to use see [PortfolioSelectorComponent]
|
||||
*
|
||||
* if [isAccountMode] is false it's mean [selectedAccount] emit [AccountId] for Main account
|
||||
*/
|
||||
interface PortfolioSelectorController {
|
||||
val isAccountMode: Flow<Boolean>
|
||||
val selectedAccount: StateFlow<AccountId?>
|
||||
val selectedAccount: Flow<AccountId?>
|
||||
val selectedAccountSync: AccountId?
|
||||
|
||||
/**
|
||||
* for some Feature specific filtering
|
||||
* combine and update with your Feature data and [PortfolioFetcher.data]
|
||||
*/
|
||||
val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean>
|
||||
|
||||
fun selectAccount(accountId: AccountId?)
|
||||
fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, AccountStatus>?>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
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
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
|
|
@ -11,6 +13,8 @@ import com.tangem.core.ui.extensions.wrappedList
|
|||
import com.tangem.core.ui.message.DialogMessage
|
||||
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.usecase.GetArchivedAccountsUseCase
|
||||
|
|
@ -20,6 +24,7 @@ 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.createedit.error.AccountFeatureError
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
|
|
@ -37,6 +42,7 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
private val recoverCryptoPortfolioUseCase: RecoverCryptoPortfolioUseCase,
|
||||
private val getArchivedAccountsUseCase: GetArchivedAccountsUseCase,
|
||||
private val umBuilder: AccountArchivedUMBuilder,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ArchivedAccountListComponent.Params>()
|
||||
|
|
@ -103,11 +109,37 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch {
|
||||
private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch(dispatchers.default) {
|
||||
recoverCryptoPortfolioUseCase(accountId)
|
||||
.onLeft { Timber.e(it.toString()) }
|
||||
.onRight { showSuccessRecoverMessage() }
|
||||
router.pop()
|
||||
.onLeft(::handleRecoverError)
|
||||
.onRight {
|
||||
showSuccessRecoverMessage()
|
||||
router.pop()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRecoverError(error: RecoverCryptoPortfolioUseCase.Error) {
|
||||
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
|
||||
return
|
||||
}
|
||||
|
||||
val featureError = AccountFeatureError.ArchivedAccountList.FailedToRecoverAccount(cause = error)
|
||||
logError(error = featureError)
|
||||
messageSender.showErrorDialog(universalError = featureError, onDismiss = router::pop)
|
||||
}
|
||||
|
||||
private fun logError(error: AccountFeatureError, params: Map<String, String> = mapOf()) {
|
||||
val exception = IllegalStateException(error.toString())
|
||||
|
||||
Timber.e(exception)
|
||||
|
||||
analyticsExceptionHandler.sendException(
|
||||
event = ExceptionAnalyticsEvent(exception = exception, params = params),
|
||||
)
|
||||
}
|
||||
|
||||
private fun showSuccessRecoverMessage() {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,18 @@
|
|||
package com.tangem.features.account.archived.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.account.ArchivedAccountListComponent
|
||||
import com.tangem.features.account.archived.ArchivedAccountListModel
|
||||
import com.tangem.features.account.archived.DefaultArchivedAccountListComponent
|
||||
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)
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface AccountArchivedModule {
|
||||
|
||||
@Binds
|
||||
fun bindArchivedAccountListComponentFactory(
|
||||
impl: DefaultArchivedAccountListComponent.Factory,
|
||||
): ArchivedAccountListComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(ArchivedAccountListModel::class)
|
||||
|
|
|
|||
|
|
@ -1,25 +1,18 @@
|
|||
package com.tangem.features.account.createedit.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.account.AccountCreateEditComponent
|
||||
import com.tangem.features.account.createedit.AccountCreateEditModel
|
||||
import com.tangem.features.account.createedit.DefaultAccountCreateEditComponent
|
||||
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)
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface AccountCreateEditModule {
|
||||
|
||||
@Binds
|
||||
fun bindAccountCreateEditComponentFactory(
|
||||
impl: DefaultAccountCreateEditComponent.Factory,
|
||||
): AccountCreateEditComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(AccountCreateEditModel::class)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ 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.usecase.ArchiveCryptoPortfolioUseCase
|
||||
import com.tangem.domain.models.PortfolioId
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
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
|
||||
|
|
@ -30,6 +33,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
private val router: Router,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<AccountDetailsComponent.Params>()
|
||||
|
|
@ -42,8 +46,11 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onManageTokensClick() {
|
||||
// todo account add account param
|
||||
router.push(AppRoute.ManageTokens(source = AppRoute.ManageTokens.Source.SETTINGS))
|
||||
val route = AppRoute.ManageTokens(
|
||||
source = AppRoute.ManageTokens.Source.SETTINGS,
|
||||
portfolioId = PortfolioId(params.account.accountId),
|
||||
)
|
||||
router.push(route)
|
||||
}
|
||||
|
||||
private fun onArchiveAccountClick() {
|
||||
|
|
@ -89,6 +96,8 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
val isMultiCurrency = getUserWalletUseCase(params.account.accountId.userWalletId)
|
||||
.getOrNull()?.isMultiCurrency ?: false
|
||||
return AccountDetailsUM(
|
||||
accountName = params.account.accountName.toUM().value,
|
||||
accountIcon = params.account.portfolioIcon.toUM(),
|
||||
|
|
@ -96,6 +105,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
onAccountEditClick = ::onEditAccountClick,
|
||||
onManageTokensClick = ::onManageTokensClick,
|
||||
archiveMode = archiveMode,
|
||||
isManageTokensAvailable = isMultiCurrency,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,18 @@
|
|||
package com.tangem.features.account.details.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.account.AccountDetailsComponent
|
||||
import com.tangem.features.account.details.AccountDetailsModel
|
||||
import com.tangem.features.account.details.DefaultAccountDetailsComponent
|
||||
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)
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface AccountDetailsModule {
|
||||
|
||||
@Binds
|
||||
fun bindAccountDetailsComponentFactory(
|
||||
impl: DefaultAccountDetailsComponent.Factory,
|
||||
): AccountDetailsComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(AccountDetailsModel::class)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ internal data class AccountDetailsUM(
|
|||
val accountName: TextReference,
|
||||
val accountIcon: CryptoPortfolioIconUM,
|
||||
val archiveMode: ArchiveMode,
|
||||
val isManageTokensAvailable: Boolean,
|
||||
val onCloseClick: () -> Unit,
|
||||
val onAccountEditClick: () -> Unit,
|
||||
val onManageTokensClick: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import com.tangem.common.ui.R
|
|||
import com.tangem.common.ui.account.AccountIconPreviewData
|
||||
import com.tangem.common.ui.account.AccountRow
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
|
|
@ -49,6 +48,7 @@ internal fun AccountDetailsContent(state: AccountDetailsUM, modifier: Modifier =
|
|||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
|
|
@ -61,21 +61,22 @@ internal fun AccountDetailsContent(state: AccountDetailsUM, modifier: Modifier =
|
|||
style = TangemTheme.typography.h1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerH16()
|
||||
AccountRow(state)
|
||||
SpacerH16()
|
||||
ManageTokensRow(state)
|
||||
if (state.isManageTokensAvailable) {
|
||||
ManageTokensRow(state)
|
||||
}
|
||||
when (state.archiveMode) {
|
||||
is AccountDetailsUM.ArchiveMode.Available -> {
|
||||
SpacerH16()
|
||||
ArchiveAccountRow(state.archiveMode)
|
||||
SpacerH(8.dp)
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
text = stringResourceSafe(R.string.account_details_archive_description),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
Column {
|
||||
ArchiveAccountRow(state.archiveMode)
|
||||
SpacerH(8.dp)
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
text = stringResourceSafe(R.string.account_details_archive_description),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
AccountDetailsUM.ArchiveMode.None -> Unit
|
||||
}
|
||||
|
|
@ -186,10 +187,12 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountD
|
|||
),
|
||||
accountName = stringReference(accountName),
|
||||
accountIcon = portfolioIcon,
|
||||
isManageTokensAvailable = true,
|
||||
)
|
||||
add(first)
|
||||
portfolioIcon = AccountIconPreviewData.randomAccountIcon(letter = true)
|
||||
add(first.copy(accountIcon = portfolioIcon))
|
||||
add(first.copy(archiveMode = AccountDetailsUM.ArchiveMode.None))
|
||||
add(first.copy(isManageTokensAvailable = false))
|
||||
},
|
||||
)
|
||||
|
|
@ -1,8 +1,14 @@
|
|||
package com.tangem.features.account.di
|
||||
|
||||
import com.tangem.features.account.AccountCreateEditComponent
|
||||
import com.tangem.features.account.AccountDetailsComponent
|
||||
import com.tangem.features.account.ArchivedAccountListComponent
|
||||
import com.tangem.features.account.PortfolioFetcher
|
||||
import com.tangem.features.account.PortfolioSelectorComponent
|
||||
import com.tangem.features.account.PortfolioSelectorController
|
||||
import com.tangem.features.account.archived.DefaultArchivedAccountListComponent
|
||||
import com.tangem.features.account.createedit.DefaultAccountCreateEditComponent
|
||||
import com.tangem.features.account.details.DefaultAccountDetailsComponent
|
||||
import com.tangem.features.account.fetcher.DefaultPortfolioFetcher
|
||||
import com.tangem.features.account.selector.DefaultPortfolioSelectorComponent
|
||||
import com.tangem.features.account.selector.DefaultPortfolioSelectorController
|
||||
|
|
@ -25,4 +31,19 @@ internal interface AccountFeatureModule {
|
|||
fun bindPortfolioSelectorComponentFactory(
|
||||
impl: DefaultPortfolioSelectorComponent.Factory,
|
||||
): PortfolioSelectorComponent.Factory
|
||||
|
||||
@Binds
|
||||
fun bindAccountCreateEditComponentFactory(
|
||||
impl: DefaultAccountCreateEditComponent.Factory,
|
||||
): AccountCreateEditComponent.Factory
|
||||
|
||||
@Binds
|
||||
fun bindAccountDetailsComponentFactory(
|
||||
impl: DefaultAccountDetailsComponent.Factory,
|
||||
): AccountDetailsComponent.Factory
|
||||
|
||||
@Binds
|
||||
fun bindArchivedAccountListComponentFactory(
|
||||
impl: DefaultArchivedAccountListComponent.Factory,
|
||||
): ArchivedAccountListComponent.Factory
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
|||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.account.PortfolioFetcher
|
||||
import com.tangem.features.account.PortfolioFetcher.*
|
||||
|
|
@ -23,7 +22,6 @@ import kotlinx.coroutines.flow.*
|
|||
internal class DefaultPortfolioFetcher @AssistedInject constructor(
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
private val getWallets: GetWalletsUseCase,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -80,14 +78,8 @@ internal class DefaultPortfolioFetcher @AssistedInject constructor(
|
|||
return combine(balanceFlows) { pairs -> pairs.toMap() }
|
||||
}
|
||||
|
||||
private fun walletAccountsBalancesFlow(wallet: UserWallet): Flow<Pair<UserWallet, PortfolioBalance>> = combine(
|
||||
flow = accountStatusListFlow(wallet),
|
||||
flow2 = getWalletTotalBalanceUseCase(wallet.walletId),
|
||||
transform = { accountStatusList, walletBalance ->
|
||||
val portfolioBalance = PortfolioBalance(walletBalance, accountStatusList)
|
||||
wallet to portfolioBalance
|
||||
},
|
||||
)
|
||||
private fun walletAccountsBalancesFlow(wallet: UserWallet): Flow<Pair<UserWallet, PortfolioBalance>> =
|
||||
accountStatusListFlow(wallet).map { wallet to PortfolioBalance(wallet, it) }
|
||||
|
||||
private fun accountStatusListFlow(wallet: UserWallet): Flow<AccountStatusList> =
|
||||
singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(wallet.walletId))
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@ internal class DefaultPortfolioSelectorComponent @AssistedInject constructor(
|
|||
.stateIn(componentScope, SharingStarted.Lazily, model.state.value.title)
|
||||
|
||||
override fun dismiss() {
|
||||
params.onDismiss()
|
||||
params.bsCallback?.onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
PortfolioSelectorBS(state, onDismiss = ::dismiss)
|
||||
PortfolioSelectorBS(state = state, onDismiss = ::dismiss, onBack = { params.bsCallback?.onBack() })
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -4,23 +4,33 @@ 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.features.account.PortfolioSelectorController
|
||||
import com.tangem.features.account.PortfolioFetcher
|
||||
import kotlinx.coroutines.flow.*
|
||||
import com.tangem.features.account.PortfolioSelectorController
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultPortfolioSelectorController @Inject constructor(
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
) : PortfolioSelectorController {
|
||||
|
||||
private val _selectedAccount: MutableStateFlow<AccountId?> = MutableStateFlow(null)
|
||||
private val _selectedAccount: MutableSharedFlow<AccountId?> = MutableSharedFlow(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
override val isAccountMode: Flow<Boolean> by lazy { isAccountsModeEnabledUseCase() }
|
||||
// without StateFlow and distinctUntilChanged to allow reselect and correct navigation
|
||||
override val selectedAccount: Flow<AccountId?> get() = _selectedAccount
|
||||
override val selectedAccountSync: AccountId? get() = _selectedAccount.replayCache.firstOrNull()
|
||||
|
||||
override val selectedAccount: StateFlow<AccountId?> get() = _selectedAccount
|
||||
override val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> = MutableStateFlow { _, _ -> true }
|
||||
|
||||
override fun selectAccount(accountId: AccountId?) {
|
||||
_selectedAccount.update { accountId }
|
||||
_selectedAccount.tryEmit(accountId)
|
||||
}
|
||||
|
||||
override fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, AccountStatus>?> =
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import com.tangem.operations.attestation.ArtworkSize
|
|||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -45,9 +44,11 @@ internal class PortfolioSelectorModel @Inject constructor(
|
|||
init {
|
||||
combine(
|
||||
flow = isAccountsModeEnabledUseCase(),
|
||||
flow2 = loadBalanceWithArtwork(),
|
||||
transform = { isAccountsMode, (portfolioData, artworks) ->
|
||||
val uiList = buildUiList(isAccountsMode, portfolioData, artworks)
|
||||
flow2 = balanceFetcher.data,
|
||||
flow3 = walletImageFetcher.allWallets(ArtworkSize.SMALL),
|
||||
flow4 = selectorController.isEnabled,
|
||||
transform = { isAccountsMode, portfolioData, artworks, isEnabled ->
|
||||
val uiList = buildUiList(isAccountsMode, portfolioData, artworks, isEnabled)
|
||||
val title = when (isAccountsMode) {
|
||||
true -> resourceReference(R.string.common_choose_account)
|
||||
false -> resourceReference(R.string.common_choose_wallet)
|
||||
|
|
@ -66,24 +67,25 @@ internal class PortfolioSelectorModel @Inject constructor(
|
|||
isAccountsMode: Boolean,
|
||||
portfolioData: PortfolioFetcher.Data,
|
||||
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
|
||||
isEnabled: (UserWallet, AccountStatus) -> Boolean,
|
||||
): List<PortfolioSelectorItemUM> = when (isAccountsMode) {
|
||||
true -> buildAccountsList(portfolioData, artworks)
|
||||
false -> buildWalletList(portfolioData, artworks)
|
||||
true -> buildAccountsList(portfolioData, artworks, isEnabled)
|
||||
false -> buildWalletList(portfolioData, artworks, isEnabled)
|
||||
}
|
||||
|
||||
private fun buildWalletList(
|
||||
portfolioData: PortfolioFetcher.Data,
|
||||
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
|
||||
isEnabled: (UserWallet, AccountStatus) -> Boolean,
|
||||
): List<PortfolioSelectorItemUM> = buildList {
|
||||
val appCurrency = portfolioData.appCurrency
|
||||
val isBalanceHidden = portfolioData.isBalanceHidden
|
||||
val lockedWallets = mutableListOf<PortfolioSelectorItemUM>()
|
||||
portfolioData.balances.forEach { wallet, portfolio ->
|
||||
val balance = portfolio.walletBalance.getOrNull()
|
||||
val balance = portfolio.walletBalance
|
||||
val walletItemUM = UserWalletItemUMConverter(
|
||||
onClick = {
|
||||
// todo account
|
||||
// selectorController.selectAccount(portfolio.accountsBalance.mainAccount)
|
||||
selectorController.selectAccount(portfolio.accountsBalance.mainAccount.account.accountId)
|
||||
},
|
||||
appCurrency = appCurrency,
|
||||
balance = balance,
|
||||
|
|
@ -92,7 +94,10 @@ internal class PortfolioSelectorModel @Inject constructor(
|
|||
isAuthMode = false,
|
||||
).convert(wallet)
|
||||
if (walletItemUM.isEnabled) {
|
||||
add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
|
||||
val isEnabledByFeature = isEnabled(wallet, portfolio.accountsBalance.mainAccount)
|
||||
val finalWalletItemUM =
|
||||
if (isEnabledByFeature) walletItemUM else walletItemUM.copy(isEnabled = false)
|
||||
add(PortfolioSelectorItemUM.Portfolio(finalWalletItemUM))
|
||||
} else {
|
||||
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
|
||||
}
|
||||
|
|
@ -110,16 +115,16 @@ internal class PortfolioSelectorModel @Inject constructor(
|
|||
private fun buildAccountsList(
|
||||
portfolioData: PortfolioFetcher.Data,
|
||||
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
|
||||
isEnabled: (UserWallet, AccountStatus) -> Boolean,
|
||||
): List<PortfolioSelectorItemUM> = buildList {
|
||||
val appCurrency = portfolioData.appCurrency
|
||||
val isBalanceHidden = portfolioData.isBalanceHidden
|
||||
val lockedWallets = mutableListOf<PortfolioSelectorItemUM>()
|
||||
portfolioData.balances.forEach { wallet, portfolio ->
|
||||
val balance = portfolio.walletBalance.getOrNull()
|
||||
val balance = portfolio.walletBalance
|
||||
val walletItemUM = UserWalletItemUMConverter(
|
||||
onClick = {
|
||||
// todo account
|
||||
// selectorController.selectAccount(portfolio.accountsBalance.mainAccount)
|
||||
selectorController.selectAccount(portfolio.accountsBalance.mainAccount.account.accountId)
|
||||
},
|
||||
appCurrency = appCurrency,
|
||||
balance = balance,
|
||||
|
|
@ -138,8 +143,8 @@ internal class PortfolioSelectorModel @Inject constructor(
|
|||
)
|
||||
add(walletTitle)
|
||||
|
||||
add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
|
||||
portfolio.accountsBalance.accountStatuses.forEach { accountStatus ->
|
||||
val isEnabledByFeature = isEnabled(wallet, accountStatus)
|
||||
val account = accountStatus.account
|
||||
val accountBalance = when (accountStatus) {
|
||||
is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance
|
||||
|
|
@ -148,6 +153,7 @@ internal class PortfolioSelectorModel @Inject constructor(
|
|||
onClick = { selectorController.selectAccount(account.accountId) },
|
||||
appCurrency = appCurrency,
|
||||
accountBalance = accountBalance,
|
||||
isEnabled = isEnabledByFeature,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
).convert(account)
|
||||
add(PortfolioSelectorItemUM.Portfolio(accountItemUM))
|
||||
|
|
@ -163,22 +169,6 @@ internal class PortfolioSelectorModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun loadBalanceWithArtwork():
|
||||
Flow<Pair<PortfolioFetcher.Data, Map<UserWalletId, UserWalletItemUM.ImageState>>> {
|
||||
val wallets = Channel<Set<UserWallet>>()
|
||||
val portfolioFlow = balanceFetcher.data
|
||||
.onEach { wallets.trySend(it.balances.keys) }
|
||||
|
||||
val artworksFlow = wallets.receiveAsFlow()
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { walletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) }
|
||||
|
||||
return combine(
|
||||
flow = portfolioFlow,
|
||||
flow2 = artworksFlow,
|
||||
) { portfolioData, artworks -> portfolioData to artworks }
|
||||
}
|
||||
|
||||
private fun emptyState() = PortfolioSelectorUM(
|
||||
items = persistentListOf(),
|
||||
title = TextReference.EMPTY,
|
||||
|
|
|
|||
|
|
@ -19,21 +19,26 @@ import com.tangem.features.account.impl.R
|
|||
import com.tangem.features.account.selector.entity.PortfolioSelectorUM
|
||||
|
||||
@Composable
|
||||
internal fun PortfolioSelectorBS(state: PortfolioSelectorUM, onDismiss: () -> Unit, modifier: Modifier = Modifier) {
|
||||
internal fun PortfolioSelectorBS(
|
||||
state: PortfolioSelectorUM,
|
||||
onDismiss: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = onDismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
onBack = onDismiss,
|
||||
onBack = onBack,
|
||||
scrollableContent = false,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
title = {
|
||||
TangemModalBottomSheetTitle(
|
||||
title = state.title,
|
||||
startIconRes = R.drawable.ic_back_24,
|
||||
onStartClick = onDismiss,
|
||||
onStartClick = onBack,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
|
|
@ -54,7 +59,8 @@ private fun Preview(@PreviewParameter(PortfolioSelectorPreviewStateProvider::cla
|
|||
PortfolioSelectorBS(
|
||||
state = params,
|
||||
onDismiss = {},
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
|
||||
modifier = Modifier,
|
||||
onBack = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,10 @@ package com.tangem.features.account.selector.ui
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
|
|
@ -18,9 +20,10 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.common.ui.account.AccountIconPreviewData
|
||||
import com.tangem.common.ui.userwallet.UserWalletItem
|
||||
import com.tangem.common.ui.userwallet.UserWalletItemRow
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM.ImageState
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
|
|
@ -66,12 +69,16 @@ internal fun PortfolioSelectorContent(
|
|||
}
|
||||
|
||||
when (item) {
|
||||
is PortfolioSelectorItemUM.Portfolio -> UserWalletItem(
|
||||
is PortfolioSelectorItemUM.Portfolio -> UserWalletItemRow(
|
||||
state = item.item,
|
||||
modifier = offsetModifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = TangemTheme.dimens.size68)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.let { if (!item.item.isEnabled) it.alpha(DISABLED_WALLET_ALPHA) else it },
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(enabled = item.item.isEnabled, onClick = item.item.onClick)
|
||||
.padding(all = TangemTheme.dimens.spacing12)
|
||||
.conditional(!item.item.isEnabled) { alpha(DISABLED_WALLET_ALPHA) },
|
||||
)
|
||||
is PortfolioSelectorItemUM.GroupTitle -> WalletNameRow(
|
||||
model = item,
|
||||
|
|
@ -103,7 +110,7 @@ private fun Preview(@PreviewParameter(PortfolioSelectorPreviewStateProvider::cla
|
|||
TangemThemePreview {
|
||||
PortfolioSelectorContent(
|
||||
state = params,
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.tertiary),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -125,9 +132,11 @@ internal object PortfolioSelectorPreviewData {
|
|||
name = accountName,
|
||||
icon = AccountIconPreviewData.randomAccountIcon(),
|
||||
),
|
||||
label = null,
|
||||
)
|
||||
|
||||
private val lockedAccountItem: UserWalletItemUM
|
||||
get() = accountItem.copy(isEnabled = false)
|
||||
|
||||
private val walletItem: UserWalletItemUM
|
||||
get() = UserWalletItemUM(
|
||||
id = UserWalletId(UUID.randomUUID().toString().encodeToByteArray()),
|
||||
|
|
@ -137,7 +146,6 @@ internal object PortfolioSelectorPreviewData {
|
|||
isEnabled = true,
|
||||
onClick = { },
|
||||
imageState = ImageState.MobileWallet,
|
||||
label = null,
|
||||
)
|
||||
|
||||
private val lockedWalletItem: UserWalletItemUM
|
||||
|
|
@ -152,7 +160,7 @@ internal object PortfolioSelectorPreviewData {
|
|||
accountItem
|
||||
.let { PortfolioSelectorItemUM.Portfolio(it) }
|
||||
.let(::add)
|
||||
accountItem
|
||||
lockedAccountItem
|
||||
.let { PortfolioSelectorItemUM.Portfolio(it) }
|
||||
.let(::add)
|
||||
PortfolioSelectorItemUM.GroupTitle(
|
||||
|
|
|
|||
|
|
@ -1,72 +1,80 @@
|
|||
package com.tangem.features.createwalletselection
|
||||
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
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.GlobalUiMessageSender
|
||||
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.components.label.entity.LabelStyle
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.analytics.IntroductionProcess
|
||||
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
|
||||
import com.tangem.domain.card.analytics.Shop
|
||||
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.createwalletselection.entity.CreateWalletSelectionUM
|
||||
import com.tangem.features.createwalletselection.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val HIDE_PROGRESS_DELAY = 400L
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class CreateWalletSelectionModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val scanCardProcessor: ScanCardProcessor,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appRouter: AppRouter,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
internal val uiState: StateFlow<CreateWalletSelectionUM>
|
||||
field = MutableStateFlow(
|
||||
CreateWalletSelectionUM(
|
||||
onBackClick = { router.pop() },
|
||||
onMobileWalletClick = ::onMobileWalletClick,
|
||||
onHardwareWalletClick = ::onHardwareWalletClick,
|
||||
onScanClick = ::onScanClick,
|
||||
blocks = persistentListOf(
|
||||
CreateWalletSelectionUM.Block(
|
||||
title = resourceReference(R.string.wallet_create_hardware_title),
|
||||
titleLabel = LabelUM(
|
||||
text = resourceReference(R.string.common_recommended),
|
||||
style = LabelStyle.ACCENT,
|
||||
),
|
||||
description = resourceReference(R.string.wallet_add_hardware_description),
|
||||
features = persistentListOf(
|
||||
CreateWalletSelectionUM.Feature(
|
||||
iconResId = R.drawable.ic_add_wallet_16,
|
||||
title = resourceReference(R.string.wallet_add_hardware_info_create),
|
||||
),
|
||||
CreateWalletSelectionUM.Feature(
|
||||
iconResId = R.drawable.ic_import_seed_16,
|
||||
title = resourceReference(R.string.wallet_add_import_seed_phrase),
|
||||
),
|
||||
),
|
||||
onClick = ::onHardwareWalletClick,
|
||||
),
|
||||
CreateWalletSelectionUM.Block(
|
||||
title = resourceReference(R.string.wallet_create_mobile_title),
|
||||
titleLabel = null,
|
||||
description = resourceReference(R.string.wallet_add_mobile_description),
|
||||
features = persistentListOf(
|
||||
CreateWalletSelectionUM.Feature(
|
||||
iconResId = R.drawable.ic_mobile_wallet_16,
|
||||
title = resourceReference(R.string.hw_create_title),
|
||||
),
|
||||
CreateWalletSelectionUM.Feature(
|
||||
iconResId = R.drawable.ic_import_seed_16,
|
||||
title = resourceReference(R.string.wallet_add_import_seed_phrase),
|
||||
),
|
||||
),
|
||||
onClick = ::onMobileWalletClick,
|
||||
),
|
||||
),
|
||||
onBuyClick = ::onBuyClick,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -86,6 +94,10 @@ internal class CreateWalletSelectionModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onHardwareWalletClick() {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
}
|
||||
|
||||
private fun onBuyClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards)
|
||||
analyticsEventHandler.send(Shop.ScreenOpened)
|
||||
modelScope.launch {
|
||||
|
|
@ -93,113 +105,6 @@ internal class CreateWalletSelectionModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onScanClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard)
|
||||
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 {
|
||||
appRouter.replaceAll(AppRoute.Wallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ifRight = {
|
||||
setLoading(false)
|
||||
sendSignedInCardAnalyticsEvent(scanResponse)
|
||||
appRouter.replaceAll(AppRoute.Wallet)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
|
||||
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(),
|
||||
hasBackup = scanResponse.card.backupStatus?.isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLoading(isLoading: Boolean) {
|
||||
uiState.update { it.copy(isScanInProgress = isLoading) }
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SHOW_ALREADY_HAVE_WALLET_DELAY = 3000L
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,26 @@
|
|||
package com.tangem.features.createwalletselection.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 CreateWalletSelectionUM(
|
||||
val isScanInProgress: Boolean = false,
|
||||
val hardwareWalletPrice: String = "$54.90",
|
||||
val showAlreadyHaveWallet: Boolean = false,
|
||||
val blocks: ImmutableList<Block>,
|
||||
val onBackClick: () -> Unit,
|
||||
val onMobileWalletClick: () -> Unit,
|
||||
val onHardwareWalletClick: () -> Unit,
|
||||
val onScanClick: () -> Unit,
|
||||
)
|
||||
val onBuyClick: () -> Unit,
|
||||
) {
|
||||
data class Block(
|
||||
val title: TextReference,
|
||||
val titleLabel: LabelUM?,
|
||||
val description: TextReference,
|
||||
val features: ImmutableList<Feature>,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
||||
data class Feature(
|
||||
val iconResId: Int,
|
||||
val title: TextReference,
|
||||
)
|
||||
}
|
||||
|
|
@ -10,23 +10,25 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
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.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM
|
||||
import com.tangem.features.createwalletselection.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
@ -34,7 +36,7 @@ import com.tangem.features.createwalletselection.impl.R
|
|||
internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding(),
|
||||
) {
|
||||
|
|
@ -42,7 +44,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi
|
|||
modifier = Modifier
|
||||
.statusBarsPadding(),
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = state.onBackClick) {
|
||||
|
|
@ -58,7 +60,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi
|
|||
Text(
|
||||
modifier = Modifier
|
||||
.padding(16.dp),
|
||||
text = stringResourceSafe(R.string.wallet_create_nav_info_title),
|
||||
text = stringResourceSafe(R.string.wallet_add_support_title),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 1,
|
||||
|
|
@ -78,60 +80,33 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi
|
|||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
text = stringResourceSafe(R.string.wallet_create_title),
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 24.dp,
|
||||
),
|
||||
text = stringResourceSafe(R.string.wallet_add_common_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
WalletBlock(
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp),
|
||||
title = stringResourceSafe(R.string.wallet_create_mobile_title),
|
||||
description = stringResourceSafe(R.string.wallet_create_mobile_description),
|
||||
badge = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.field.focused,
|
||||
shape = TangemTheme.shapes.roundedCorners8,
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_free),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = state.onMobileWalletClick,
|
||||
)
|
||||
WalletBlock(
|
||||
title = stringResourceSafe(R.string.wallet_create_hardware_title),
|
||||
description = stringResourceSafe(R.string.wallet_create_hardware_description),
|
||||
badge = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.text.accent.copy(alpha = 0.1f),
|
||||
shape = TangemTheme.shapes.roundedCorners8,
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.wallet_create_hardware_badge, state.hardwareWalletPrice),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = state.onHardwareWalletClick,
|
||||
)
|
||||
state.blocks.forEach { block ->
|
||||
WalletBlock(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp),
|
||||
title = block.title.resolveReference(),
|
||||
description = block.description.resolveReference(),
|
||||
features = block.features,
|
||||
badge = block.titleLabel?.let {
|
||||
{ Label(it) }
|
||||
},
|
||||
onClick = block.onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(state.showAlreadyHaveWallet) {
|
||||
AlreadyHaveTangemWalletBlock(
|
||||
onScanClick = state.onScanClick,
|
||||
onBuyClick = state.onBuyClick,
|
||||
isScanInProgress = state.isScanInProgress,
|
||||
)
|
||||
}
|
||||
|
|
@ -143,8 +118,9 @@ private fun WalletBlock(
|
|||
title: String,
|
||||
description: String,
|
||||
onClick: () -> Unit,
|
||||
features: ImmutableList<CreateWalletSelectionUM.Feature>,
|
||||
modifier: Modifier = Modifier,
|
||||
badge: @Composable () -> Unit,
|
||||
badge: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
|
@ -152,11 +128,14 @@ private fun WalletBlock(
|
|||
.padding(top = 8.dp)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(
|
||||
color = TangemTheme.colors.field.primary,
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(16.dp),
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 12.dp,
|
||||
),
|
||||
) {
|
||||
Row {
|
||||
Text(
|
||||
|
|
@ -167,7 +146,7 @@ private fun WalletBlock(
|
|||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
badge()
|
||||
badge?.invoke()
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier
|
||||
|
|
@ -176,24 +155,57 @@ private fun WalletBlock(
|
|||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
if (features.isNotEmpty()) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
thickness = 0.5.dp,
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
)
|
||||
features.forEach {
|
||||
Feature(
|
||||
feature = it,
|
||||
modifier = Modifier
|
||||
.padding(top = 12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Feature(feature: CreateWalletSelectionUM.Feature, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size16),
|
||||
painter = painterResource(id = feature.iconResId),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.padding(start = 6.dp),
|
||||
text = feature.title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AlreadyHaveTangemWalletBlock(
|
||||
onScanClick: () -> Unit,
|
||||
onBuyClick: () -> Unit,
|
||||
isScanInProgress: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var buttonWidth by remember { mutableStateOf(0) }
|
||||
val density = LocalDensity.current
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.background(
|
||||
color = TangemTheme.colors.field.primary,
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.padding(
|
||||
|
|
@ -206,30 +218,16 @@ private fun AlreadyHaveTangemWalletBlock(
|
|||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 16.dp),
|
||||
text = stringResourceSafe(R.string.wallet_create_scan_question),
|
||||
text = stringResourceSafe(R.string.wallet_add_hardware_purchase),
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
TangemButton(
|
||||
modifier = Modifier
|
||||
.conditional(buttonWidth > 0) {
|
||||
width(with(density) { buttonWidth.toDp() })
|
||||
}
|
||||
.onGloballyPositioned { coordinates ->
|
||||
if (buttonWidth == 0) {
|
||||
buttonWidth = coordinates.size.width
|
||||
}
|
||||
},
|
||||
text = stringResourceSafe(R.string.wallet_create_scan_title),
|
||||
onClick = onScanClick,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
|
||||
SecondaryButton(
|
||||
text = stringResourceSafe(R.string.wallet_import_buy_title),
|
||||
onClick = onBuyClick,
|
||||
size = TangemButtonSize.RoundedAction,
|
||||
showProgress = isScanInProgress,
|
||||
colors = TangemButtonsDefaults.secondaryButtonColors,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
enabled = true,
|
||||
animateContentChange = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -241,11 +239,45 @@ private fun PreviewCreateWalletContent() {
|
|||
TangemThemePreview {
|
||||
CreateWalletSelectionContent(
|
||||
state = CreateWalletSelectionUM(
|
||||
showAlreadyHaveWallet = true,
|
||||
onBackClick = {},
|
||||
onMobileWalletClick = {},
|
||||
onHardwareWalletClick = {},
|
||||
onScanClick = {},
|
||||
onBackClick = { },
|
||||
blocks = persistentListOf(
|
||||
CreateWalletSelectionUM.Block(
|
||||
title = resourceReference(R.string.wallet_create_hardware_title),
|
||||
titleLabel = LabelUM(
|
||||
text = resourceReference(R.string.common_recommended),
|
||||
style = LabelStyle.ACCENT,
|
||||
),
|
||||
description = resourceReference(R.string.wallet_add_hardware_description),
|
||||
features = persistentListOf(
|
||||
CreateWalletSelectionUM.Feature(
|
||||
iconResId = R.drawable.ic_add_wallet_16,
|
||||
title = resourceReference(R.string.wallet_add_hardware_info_create),
|
||||
),
|
||||
CreateWalletSelectionUM.Feature(
|
||||
iconResId = R.drawable.ic_import_seed_16,
|
||||
title = resourceReference(R.string.wallet_add_import_seed_phrase),
|
||||
),
|
||||
),
|
||||
onClick = { },
|
||||
),
|
||||
CreateWalletSelectionUM.Block(
|
||||
title = resourceReference(R.string.wallet_create_mobile_title),
|
||||
titleLabel = null,
|
||||
description = resourceReference(R.string.wallet_add_mobile_description),
|
||||
features = persistentListOf(
|
||||
CreateWalletSelectionUM.Feature(
|
||||
iconResId = R.drawable.ic_mobile_wallet_16,
|
||||
title = resourceReference(R.string.hw_create_title),
|
||||
),
|
||||
CreateWalletSelectionUM.Feature(
|
||||
iconResId = R.drawable.ic_import_seed_16,
|
||||
title = resourceReference(R.string.wallet_add_import_seed_phrase),
|
||||
),
|
||||
),
|
||||
onClick = { },
|
||||
),
|
||||
),
|
||||
onBuyClick = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
1
features/create-wallet-start/api/.gitignore
vendored
Normal file
1
features/create-wallet-start/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
21
features/create-wallet-start/api/build.gradle.kts
Normal file
21
features/create-wallet-start/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.createwalletstart.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.runtime)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.createwalletstart
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
interface CreateWalletStartComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val mode: Mode,
|
||||
)
|
||||
|
||||
enum class Mode {
|
||||
ColdWallet,
|
||||
HotWallet,
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, CreateWalletStartComponent>
|
||||
}
|
||||
1
features/create-wallet-start/impl/.gitignore
vendored
Normal file
1
features/create-wallet-start/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
72
features/create-wallet-start/impl/build.gradle.kts
Normal file
72
features/create-wallet-start/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.createwalletstart.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Api */
|
||||
implementation(projects.features.createWalletStart.api)
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.datasource)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.routing)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(projects.libs.tangemSdkApi)
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.card.android) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.core.ktx)
|
||||
implementation(deps.lifecycle.runtime.ktx)
|
||||
|
||||
/** Compose libraries */
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.animation)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.lottie.compose)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.androidx.activity.compose)
|
||||
implementation(deps.androidx.datastore)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.firebase.crashlytics)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
package com.tangem.features.createwalletstart
|
||||
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
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.GlobalUiMessageSender
|
||||
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.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.createwalletstart.entity.CreateWalletStartUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val HIDE_PROGRESS_DELAY = 400L
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class CreateWalletStartModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val scanCardProcessor: ScanCardProcessor,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appRouter: AppRouter,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<CreateWalletStartComponent.Params>()
|
||||
|
||||
internal val uiState: StateFlow<CreateWalletStartUM>
|
||||
field = MutableStateFlow(
|
||||
when (params.mode) {
|
||||
CreateWalletStartComponent.Mode.ColdWallet -> CreateWalletStartUM(
|
||||
title = resourceReference(R.string.common_tangem_wallet),
|
||||
description = resourceReference(R.string.welcome_create_wallet_hardware_description),
|
||||
featureItems = persistentListOf(
|
||||
CreateWalletStartUM.FeatureItem(
|
||||
iconResId = R.drawable.ic_shield_check_16,
|
||||
text = resourceReference(R.string.welcome_create_wallet_feature_class),
|
||||
),
|
||||
CreateWalletStartUM.FeatureItem(
|
||||
iconResId = R.drawable.ic_flash_16,
|
||||
text = resourceReference(R.string.welcome_create_wallet_feature_delivery),
|
||||
),
|
||||
CreateWalletStartUM.FeatureItem(
|
||||
iconResId = R.drawable.ic_sparkles_16,
|
||||
text = resourceReference(R.string.welcome_create_wallet_feature_use),
|
||||
),
|
||||
),
|
||||
imageResId = R.drawable.img_hardware_wallet,
|
||||
showScanSecondaryButton = true,
|
||||
onPrimaryButtonClick = ::onBuyClick,
|
||||
primaryButtonText = resourceReference(R.string.details_buy_wallet),
|
||||
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title),
|
||||
otherMethodDescription = resourceReference(R.string.welcome_create_wallet_mobile_description),
|
||||
otherMethodClick = ::onStartWithMobileWalletClick,
|
||||
onBackClick = { router.pop() },
|
||||
onScanClick = ::onScanClick,
|
||||
isScanInProgress = false,
|
||||
)
|
||||
CreateWalletStartComponent.Mode.HotWallet -> CreateWalletStartUM(
|
||||
title = resourceReference(R.string.hw_mobile_wallet),
|
||||
description = resourceReference(R.string.welcome_create_wallet_mobile_description_full),
|
||||
featureItems = persistentListOf(
|
||||
CreateWalletStartUM.FeatureItem(
|
||||
iconResId = R.drawable.ic_shield_check_16,
|
||||
text = resourceReference(R.string.welcome_create_wallet_feature_seamless),
|
||||
),
|
||||
CreateWalletStartUM.FeatureItem(
|
||||
iconResId = R.drawable.ic_flash_16,
|
||||
text = resourceReference(R.string.welcome_create_wallet_feature_one_tap),
|
||||
),
|
||||
CreateWalletStartUM.FeatureItem(
|
||||
iconResId = R.drawable.ic_stack_fill_new_16,
|
||||
text = resourceReference(R.string.welcome_create_wallet_feature_assets),
|
||||
),
|
||||
),
|
||||
imageResId = R.drawable.img_mobile_wallet,
|
||||
showScanSecondaryButton = false,
|
||||
onPrimaryButtonClick = ::onStartWithMobileWalletClick,
|
||||
primaryButtonText = resourceReference(R.string.welcome_create_wallet_mobile_title),
|
||||
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_use_hardware_title),
|
||||
otherMethodDescription = resourceReference(R.string.welcome_create_wallet_use_hardware_description),
|
||||
otherMethodClick = ::onBuyClick,
|
||||
onBackClick = { router.pop() },
|
||||
onScanClick = ::onScanClick,
|
||||
isScanInProgress = false,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
private fun onScanClick() {
|
||||
scanCard()
|
||||
}
|
||||
|
||||
private fun onStartWithMobileWalletClick() {
|
||||
router.push(AppRoute.CreateMobileWallet)
|
||||
}
|
||||
|
||||
private fun onBuyClick() {
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
appRouter.replaceAll(AppRoute.Wallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ifRight = {
|
||||
setLoading(false)
|
||||
sendSignedInCardAnalyticsEvent(scanResponse = scanResponse, isImported = userWallet.isImported)
|
||||
appRouter.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,42 @@
|
|||
package com.tangem.features.createwalletstart
|
||||
|
||||
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.core.ui.components.SystemBarsIconsDisposable
|
||||
import com.tangem.core.ui.res.ForceDarkTheme
|
||||
import com.tangem.features.createwalletstart.ui.CreateWalletStartContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultCreateWalletStartComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: CreateWalletStartComponent.Params,
|
||||
) : CreateWalletStartComponent, AppComponentContext by context {
|
||||
|
||||
private val model: CreateWalletStartModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
SystemBarsIconsDisposable(darkIcons = false)
|
||||
ForceDarkTheme {
|
||||
CreateWalletStartContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : CreateWalletStartComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: CreateWalletStartComponent.Params,
|
||||
): DefaultCreateWalletStartComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.features.createwalletstart.di
|
||||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.createwalletstart.CreateWalletStartComponent
|
||||
import com.tangem.features.createwalletstart.CreateWalletStartModel
|
||||
import com.tangem.features.createwalletstart.DefaultCreateWalletStartComponent
|
||||
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 CreateWalletStartModule
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface CreateWalletStartModuleBinds {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindCreateWalletStartComponentFactory(
|
||||
impl: DefaultCreateWalletStartComponent.Factory,
|
||||
): CreateWalletStartComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(CreateWalletStartModel::class)
|
||||
fun bindCreateWalletStartModel(model: CreateWalletStartModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.features.createwalletstart.entity
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class CreateWalletStartUM(
|
||||
val title: TextReference,
|
||||
val description: TextReference,
|
||||
val featureItems: ImmutableList<FeatureItem>,
|
||||
val imageResId: Int,
|
||||
val isScanInProgress: Boolean,
|
||||
val showScanSecondaryButton: Boolean,
|
||||
val primaryButtonText: TextReference,
|
||||
val onPrimaryButtonClick: () -> Unit,
|
||||
val otherMethodDescription: TextReference,
|
||||
val otherMethodTitle: TextReference,
|
||||
val otherMethodClick: () -> Unit,
|
||||
val onScanClick: () -> Unit,
|
||||
val onBackClick: () -> Unit,
|
||||
) {
|
||||
data class FeatureItem(
|
||||
val iconResId: Int,
|
||||
val text: TextReference,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,486 @@
|
|||
package com.tangem.features.createwalletstart.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
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 androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SecondaryButtonIconEnd
|
||||
import com.tangem.core.ui.components.bottomFade
|
||||
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.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.createwalletstart.entity.CreateWalletStartUM
|
||||
import com.tangem.features.createwalletstart.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlin.math.max
|
||||
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
listOf(
|
||||
TangemColorPalette.Dark6,
|
||||
TangemColorPalette.Black,
|
||||
),
|
||||
),
|
||||
)
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
TopAppBar(
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = state.onBackClick) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_back_24),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
title = { },
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.bottomFade(height = 24.dp),
|
||||
) {
|
||||
AdaptiveScrollableContent(
|
||||
topContent = {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 32.dp,
|
||||
top = 16.dp,
|
||||
end = 32.dp,
|
||||
),
|
||||
text = state.title.resolveReference(),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 32.dp,
|
||||
top = 8.dp,
|
||||
end = 32.dp,
|
||||
),
|
||||
text = state.description.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
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),
|
||||
) {
|
||||
state.featureItems.forEach {
|
||||
FeatureItem(
|
||||
iconResId = it.iconResId,
|
||||
text = it.text,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
imageContent = {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(
|
||||
vertical = 12.dp,
|
||||
horizontal = 16.dp,
|
||||
),
|
||||
painter = painterResource(id = state.imageResId),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
if (state.showScanSecondaryButton) {
|
||||
SecondaryButtonIconEnd(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
text = stringResourceSafe(R.string.welcome_unlock_card),
|
||||
onClick = state.onScanClick,
|
||||
showProgress = state.isScanInProgress,
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
)
|
||||
}
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
top = 8.dp,
|
||||
end = 16.dp,
|
||||
),
|
||||
text = state.primaryButtonText.resolveReference(),
|
||||
onClick = state.onPrimaryButtonClick,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
top = 24.dp,
|
||||
end = 16.dp,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
DashedGradientLine(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(16.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.welcome_create_wallet_other_method),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
DashedGradientLine(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(16.dp)
|
||||
.scale(scaleX = -1f, scaleY = 1f),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
top = 16.dp,
|
||||
end = 16.dp,
|
||||
),
|
||||
text = state.otherMethodDescription.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.clickable { state.otherMethodClick() }
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 12.dp,
|
||||
),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = state.otherMethodTitle.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_chevron_right_18x24),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
minImageHeight = 160.dp,
|
||||
)
|
||||
}
|
||||
if (!state.showScanSecondaryButton) {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
top = 24.dp,
|
||||
end = 16.dp,
|
||||
bottom = 8.dp,
|
||||
),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.welcome_create_wallet_already_have),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
) { state.onScanClick() },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.wallet_create_scan_title),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Spacer(modifier = Modifier.size(2.dp))
|
||||
Icon(
|
||||
modifier = Modifier.size(16.dp),
|
||||
painter = painterResource(id = R.drawable.ic_tangem_24),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||
@Composable
|
||||
private fun AdaptiveScrollableContent(
|
||||
minImageHeight: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
topContent: @Composable () -> Unit,
|
||||
imageContent: @Composable () -> Unit,
|
||||
bottomContent: @Composable () -> Unit,
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val viewportHeight = maxHeight
|
||||
val minImageHeightPx = with(density) { minImageHeight.roundToPx() }
|
||||
val viewportHeightPx = with(density) { viewportHeight.roundToPx() }
|
||||
Layout(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
topContent()
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
imageContent()
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
bottomContent()
|
||||
}
|
||||
},
|
||||
) { measurables, constraints ->
|
||||
val topPlaceable = measurables[0].measure(
|
||||
constraints.copy(minHeight = 0, maxHeight = androidx.compose.ui.unit.Constraints.Infinity),
|
||||
)
|
||||
val bottomPlaceable = measurables[2].measure(
|
||||
constraints.copy(minHeight = 0, maxHeight = androidx.compose.ui.unit.Constraints.Infinity),
|
||||
)
|
||||
val imageIntrinsicHeight = measurables[1].maxIntrinsicHeight(constraints.maxWidth)
|
||||
val availableHeightForImage = max(0, viewportHeightPx - topPlaceable.height - bottomPlaceable.height)
|
||||
val targetImageHeight = when {
|
||||
imageIntrinsicHeight < minImageHeightPx -> minImageHeightPx
|
||||
imageIntrinsicHeight > availableHeightForImage -> max(minImageHeightPx, availableHeightForImage)
|
||||
else -> imageIntrinsicHeight
|
||||
}
|
||||
val imagePlaceable = measurables[1].measure(
|
||||
constraints.copy(
|
||||
minHeight = targetImageHeight,
|
||||
maxHeight = targetImageHeight,
|
||||
),
|
||||
)
|
||||
val totalContentHeight = topPlaceable.height + imagePlaceable.height + bottomPlaceable.height
|
||||
layout(constraints.maxWidth, totalContentHeight) {
|
||||
var yOffset = 0
|
||||
topPlaceable.placeRelative(0, yOffset)
|
||||
yOffset += topPlaceable.height
|
||||
imagePlaceable.placeRelative(0, yOffset)
|
||||
yOffset += imagePlaceable.height
|
||||
bottomPlaceable.placeRelative(0, yOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DashedGradientLine(modifier: Modifier = Modifier) {
|
||||
val density = LocalDensity.current
|
||||
|
||||
val strokeColor = TangemTheme.colors.stroke.primary
|
||||
|
||||
Canvas(modifier = modifier) {
|
||||
val strokePx = with(density) { 4.dp.toPx() }
|
||||
val dashPx = with(density) { 4.dp.toPx() }
|
||||
val gapPx = with(density) { 8.dp.toPx() }
|
||||
|
||||
val width = size.width
|
||||
val centerY = size.height / 2
|
||||
|
||||
val brush = Brush.linearGradient(
|
||||
colors = listOf(strokeColor.copy(alpha = 0f), strokeColor),
|
||||
start = Offset(0f, 0f),
|
||||
end = Offset(width, 0f),
|
||||
)
|
||||
|
||||
val pathEffect = PathEffect.dashPathEffect(floatArrayOf(dashPx, gapPx), 0f)
|
||||
|
||||
drawLine(
|
||||
brush = brush,
|
||||
start = Offset(0f, centerY),
|
||||
end = Offset(width, centerY),
|
||||
strokeWidth = strokePx,
|
||||
pathEffect = pathEffect,
|
||||
cap = StrokeCap.Round,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class CreateWalletStartStateProvider : CollectionPreviewParameterProvider<CreateWalletStartUM>(
|
||||
collection = listOf(
|
||||
CreateWalletStartUM(
|
||||
title = resourceReference(R.string.common_tangem_wallet),
|
||||
description = resourceReference(R.string.welcome_create_wallet_hardware_description),
|
||||
featureItems = persistentListOf(
|
||||
CreateWalletStartUM.FeatureItem(
|
||||
iconResId = R.drawable.ic_shield_check_16,
|
||||
text = resourceReference(R.string.welcome_create_wallet_feature_class),
|
||||
),
|
||||
CreateWalletStartUM.FeatureItem(
|
||||
iconResId = R.drawable.ic_flash_16,
|
||||
text = resourceReference(R.string.welcome_create_wallet_feature_delivery),
|
||||
),
|
||||
CreateWalletStartUM.FeatureItem(
|
||||
iconResId = R.drawable.ic_sparkles_16,
|
||||
text = resourceReference(R.string.welcome_create_wallet_feature_use),
|
||||
),
|
||||
),
|
||||
imageResId = R.drawable.img_hardware_wallet,
|
||||
showScanSecondaryButton = true,
|
||||
onPrimaryButtonClick = { },
|
||||
primaryButtonText = resourceReference(R.string.details_buy_wallet),
|
||||
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title),
|
||||
otherMethodDescription = resourceReference(
|
||||
R.string.welcome_create_wallet_mobile_description,
|
||||
),
|
||||
otherMethodClick = { },
|
||||
onBackClick = { },
|
||||
onScanClick = { },
|
||||
isScanInProgress = false,
|
||||
),
|
||||
CreateWalletStartUM(
|
||||
title = resourceReference(R.string.hw_mobile_wallet),
|
||||
description = resourceReference(R.string.welcome_create_wallet_mobile_description_full),
|
||||
featureItems = persistentListOf(
|
||||
CreateWalletStartUM.FeatureItem(
|
||||
iconResId = R.drawable.ic_shield_check_16,
|
||||
text = resourceReference(R.string.welcome_create_wallet_feature_seamless),
|
||||
),
|
||||
CreateWalletStartUM.FeatureItem(
|
||||
iconResId = R.drawable.ic_flash_16,
|
||||
text = resourceReference(R.string.welcome_create_wallet_feature_one_tap),
|
||||
),
|
||||
CreateWalletStartUM.FeatureItem(
|
||||
iconResId = R.drawable.ic_stack_fill_new_16,
|
||||
text = resourceReference(R.string.welcome_create_wallet_feature_assets),
|
||||
),
|
||||
),
|
||||
imageResId = R.drawable.img_mobile_wallet,
|
||||
showScanSecondaryButton = false,
|
||||
onPrimaryButtonClick = { },
|
||||
primaryButtonText = resourceReference(R.string.welcome_create_wallet_mobile_title),
|
||||
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_use_hardware_title),
|
||||
otherMethodDescription = resourceReference(
|
||||
R.string.welcome_create_wallet_use_hardware_description,
|
||||
),
|
||||
otherMethodClick = { },
|
||||
onBackClick = { },
|
||||
onScanClick = { },
|
||||
isScanInProgress = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 480, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 560, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 840, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewCreateWalletStartContent(
|
||||
@PreviewParameter(CreateWalletStartStateProvider::class) param: CreateWalletStartUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
CreateWalletStartContent(
|
||||
state = param,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package com.tangem.features.details.entity
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
@ -12,5 +11,4 @@ internal data class UserWalletListUM(
|
|||
val isWalletSavingInProgress: Boolean,
|
||||
val addNewWalletText: TextReference,
|
||||
val onAddNewWalletClick: () -> Unit,
|
||||
val addWalletBottomSheet: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty,
|
||||
)
|
||||
|
|
@ -6,14 +6,8 @@ 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.components.bottomsheets.BottomSheetOption
|
||||
import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheetContent
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
|
||||
import com.tangem.features.details.entity.UserWalletListUM
|
||||
import com.tangem.features.details.impl.R
|
||||
|
|
@ -27,7 +21,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -38,8 +31,6 @@ internal class UserWalletListModel @Inject constructor(
|
|||
private val router: Router,
|
||||
private val messageSender: UiMessageSender,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val userWalletSaver: UserWalletSaver,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) : Model() {
|
||||
|
|
@ -58,7 +49,6 @@ internal class UserWalletListModel @Inject constructor(
|
|||
isWalletSavingInProgress = false,
|
||||
addNewWalletText = TextReference.EMPTY,
|
||||
onAddNewWalletClick = ::onAddNewWalletClick,
|
||||
addWalletBottomSheet = TangemBottomSheetConfig.Empty,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -90,62 +80,11 @@ internal class UserWalletListModel @Inject constructor(
|
|||
|
||||
private fun onAddNewWalletClick() {
|
||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
addWalletBottomSheet = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = ::dismissAddWalletBottomSheet,
|
||||
content = createAddWalletBottomSheetContent(),
|
||||
),
|
||||
)
|
||||
}
|
||||
router.push(AppRoute.CreateWalletSelection)
|
||||
} else {
|
||||
withProgress(isWalletSavingInProgress) {
|
||||
userWalletSaver.scanAndSaveUserWallet(modelScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissAddWalletBottomSheet() {
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
addWalletBottomSheet = currentState.addWalletBottomSheet.copy(isShown = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAddWalletBottomSheetContent(): OptionsBottomSheetContent {
|
||||
return OptionsBottomSheetContent(
|
||||
options = persistentListOf(
|
||||
BottomSheetOption(
|
||||
key = ADD_WALLET_KEY_CREATE,
|
||||
label = resourceReference(string.home_button_create_new_wallet),
|
||||
),
|
||||
BottomSheetOption(
|
||||
key = ADD_WALLET_KEY_ADD,
|
||||
label = resourceReference(string.home_button_add_existing_wallet),
|
||||
),
|
||||
BottomSheetOption(
|
||||
key = ADD_WALLET_KEY_BUY,
|
||||
label = resourceReference(string.details_buy_wallet),
|
||||
),
|
||||
),
|
||||
onOptionClick = { optionKey ->
|
||||
dismissAddWalletBottomSheet()
|
||||
when (optionKey) {
|
||||
ADD_WALLET_KEY_CREATE -> router.push(AppRoute.CreateWalletSelection)
|
||||
ADD_WALLET_KEY_ADD -> router.push(AppRoute.AddExistingWallet)
|
||||
ADD_WALLET_KEY_BUY -> modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ADD_WALLET_KEY_CREATE = "create"
|
||||
private const val ADD_WALLET_KEY_ADD = "add"
|
||||
private const val ADD_WALLET_KEY_BUY = "buy"
|
||||
}
|
||||
}
|
||||
|
|
@ -15,13 +15,9 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.userwallet.UserWalletItem
|
||||
import com.tangem.core.ui.R.*
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
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.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.details.component.UserWalletListComponent
|
||||
|
|
@ -48,8 +44,6 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M
|
|||
onClick = state.onAddNewWalletClick,
|
||||
)
|
||||
}
|
||||
|
||||
AddWalletBottomSheet(state.addWalletBottomSheet)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -100,15 +94,6 @@ private fun AddWalletButton(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddWalletBottomSheet(config: TangemBottomSheetConfig) {
|
||||
OptionsBottomSheet(
|
||||
config = config,
|
||||
title = resourceReference(string.auth_info_add_wallet_title),
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
|
|
|
|||
|
|
@ -86,8 +86,7 @@ internal class HomeModel @Inject constructor(
|
|||
onScanClick = ::onScanClick,
|
||||
onShopClick = ::onShopClick,
|
||||
onSearchTokensClick = ::onSearchTokensClick,
|
||||
onCreateNewWalletClick = ::onCreateNewWalletClick,
|
||||
onAddExistingWalletClick = ::onAddExistingWalletClick,
|
||||
onGetStartedClick = ::onGetStartedClick,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -145,12 +144,8 @@ internal class HomeModel @Inject constructor(
|
|||
router.push(AppRoute.ManageTokens(Source.STORIES))
|
||||
}
|
||||
|
||||
private fun onCreateNewWalletClick() {
|
||||
router.push(AppRoute.CreateWalletSelection)
|
||||
}
|
||||
|
||||
private fun onAddExistingWalletClick() {
|
||||
router.push(AppRoute.AddExistingWallet)
|
||||
private fun onGetStartedClick() {
|
||||
router.push(AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.ColdWallet))
|
||||
}
|
||||
|
||||
private fun scanCard() {
|
||||
|
|
@ -207,13 +202,13 @@ internal class HomeModel @Inject constructor(
|
|||
ifRight = {
|
||||
reduxStateHolder.onUserWalletSelected(userWallet)
|
||||
setLoading(false)
|
||||
sendSignedInCardAnalyticsEvent(scanResponse)
|
||||
sendSignedInCardAnalyticsEvent(scanResponse, userWallet.isImported)
|
||||
appRouter.replaceAll(AppRoute.Wallet)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
|
||||
private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse, isImported: Boolean) {
|
||||
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
|
||||
if (currency != null) {
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -222,6 +217,7 @@ internal class HomeModel @Inject constructor(
|
|||
batch = scanResponse.card.batchId,
|
||||
signInType = SignInType.Card,
|
||||
walletsCount = getWalletsCount().toString(),
|
||||
isImported = isImported,
|
||||
hasBackup = scanResponse.card.backupStatus?.isActive,
|
||||
),
|
||||
)
|
||||
|
|
@ -240,7 +236,7 @@ internal class HomeModel @Inject constructor(
|
|||
_uiState.update { it.copy(scanInProgress = isLoading) }
|
||||
}
|
||||
|
||||
fun handleScanError(error: TangemError) {
|
||||
private fun handleScanError(error: TangemError) {
|
||||
when (error) {
|
||||
is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable()
|
||||
is TangemSdkError -> Timber.e(error, "Scan error occurred")
|
||||
|
|
|
|||
|
|
@ -17,9 +17,7 @@ internal fun Home(state: HomeUM, isV2StoriesEnabled: Boolean, modifier: Modifier
|
|||
StoriesScreenV2(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onCreateNewWalletButtonClick = state.onCreateNewWalletClick,
|
||||
onAddExistingWalletButtonClick = state.onAddExistingWalletClick,
|
||||
onScanButtonClick = state.onScanClick,
|
||||
onGetStartedClick = state.onGetStartedClick,
|
||||
)
|
||||
} else {
|
||||
StoriesScreen(
|
||||
|
|
|
|||
|
|
@ -29,13 +29,7 @@ import com.tangem.core.ui.R
|
|||
import com.tangem.features.home.impl.ui.state.HomeUM
|
||||
|
||||
@Composable
|
||||
internal fun StoriesScreenV2(
|
||||
state: HomeUM,
|
||||
onCreateNewWalletButtonClick: () -> Unit,
|
||||
onAddExistingWalletButtonClick: () -> Unit,
|
||||
onScanButtonClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
internal fun StoriesScreenV2(state: HomeUM, onGetStartedClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
var currentStory by remember { mutableStateOf(state.firstStory) }
|
||||
val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory))
|
||||
|
||||
|
|
@ -64,9 +58,7 @@ internal fun StoriesScreenV2(
|
|||
isScanInProgress = state.scanInProgress,
|
||||
onGoToPreviousStory = goToPreviousStory,
|
||||
onGoToNextStory = goToNextStory,
|
||||
onCreateNewWalletButtonClick = onCreateNewWalletButtonClick,
|
||||
onAddExistingWalletButtonClick = onAddExistingWalletButtonClick,
|
||||
onScanButtonClick = onScanButtonClick,
|
||||
onGetStartedClick = onGetStartedClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -176,10 +168,7 @@ private fun StoriesScreenContentV2(config: StoriesScreenContentV2Config, modifie
|
|||
) {
|
||||
HomeButtonsV2(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
btnScanStateInProgress = config.isScanInProgress,
|
||||
onScanButtonClick = config.onScanButtonClick,
|
||||
onCreateNewWalletButtonClick = config.onCreateNewWalletButtonClick,
|
||||
onAddExistingWalletButtonClick = config.onAddExistingWalletButtonClick,
|
||||
onGetStartedClick = config.onGetStartedClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -192,9 +181,7 @@ private data class StoriesScreenContentV2Config(
|
|||
val isScanInProgress: Boolean,
|
||||
val onGoToPreviousStory: () -> Unit = {},
|
||||
val onGoToNextStory: () -> Unit = {},
|
||||
val onCreateNewWalletButtonClick: () -> Unit = {},
|
||||
val onAddExistingWalletButtonClick: () -> Unit = {},
|
||||
val onScanButtonClick: () -> Unit = {},
|
||||
val onGetStartedClick: () -> Unit = {},
|
||||
)
|
||||
|
||||
// region Preview
|
||||
|
|
|
|||
|
|
@ -9,116 +9,42 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
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.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.StoriesScreenTestTags
|
||||
import com.tangem.core.ui.R
|
||||
|
||||
@Composable
|
||||
internal fun HomeButtonsV2(
|
||||
btnScanStateInProgress: Boolean,
|
||||
onScanButtonClick: () -> Unit,
|
||||
onCreateNewWalletButtonClick: () -> Unit,
|
||||
onAddExistingWalletButtonClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
internal fun HomeButtonsV2(onGetStartedClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
CreateNewWalletButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(StoriesScreenTestTags.CREATE_NEW_WALLET_BUTTON),
|
||||
onClick = onCreateNewWalletButtonClick,
|
||||
)
|
||||
AddExistingWalletButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(StoriesScreenTestTags.ADD_EXISTING_WALLET_BUTTON),
|
||||
onClick = onAddExistingWalletButtonClick,
|
||||
)
|
||||
ScanCardButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(StoriesScreenTestTags.SCAN_BUTTON),
|
||||
showProgress = btnScanStateInProgress,
|
||||
onClick = onScanButtonClick,
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.common_get_started),
|
||||
useDarkerColors = false,
|
||||
onClick = onGetStartedClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreateNewWalletButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.home_button_create_new_wallet),
|
||||
useDarkerColors = false,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddExistingWalletButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.home_button_add_existing_wallet),
|
||||
useDarkerColors = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.home_button_scan),
|
||||
useDarkerColors = true,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
|
||||
onClick = onClick,
|
||||
showProgress = showProgress,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun HomeButtonsV2Preview(@PreviewParameter(HomeButtonsV2ParameterProvider::class) state: HomeButtonsV2State) {
|
||||
private fun HomeButtonsV2Preview() {
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
modifier = Modifier.background(Color.Black),
|
||||
) {
|
||||
HomeButtonsV2(
|
||||
btnScanStateInProgress = state.btnScanStateInProgress,
|
||||
onCreateNewWalletButtonClick = {},
|
||||
onAddExistingWalletButtonClick = {},
|
||||
onScanButtonClick = {},
|
||||
onGetStartedClick = {},
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class HomeButtonsV2ParameterProvider : CollectionPreviewParameterProvider<HomeButtonsV2State>(
|
||||
collection = listOf(
|
||||
HomeButtonsV2State(
|
||||
btnScanStateInProgress = false,
|
||||
),
|
||||
HomeButtonsV2State(
|
||||
btnScanStateInProgress = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private data class HomeButtonsV2State(
|
||||
val btnScanStateInProgress: Boolean,
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -8,8 +8,7 @@ data class HomeUM(
|
|||
val onScanClick: () -> Unit,
|
||||
val onShopClick: () -> Unit,
|
||||
val onSearchTokensClick: () -> Unit,
|
||||
val onCreateNewWalletClick: () -> Unit,
|
||||
val onAddExistingWalletClick: () -> Unit,
|
||||
val onGetStartedClick: () -> Unit,
|
||||
) {
|
||||
val firstStory: Stories get() = stories[0]
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.settings.CanUseBiometryUseCase
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS
|
||||
|
|
@ -31,6 +32,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val canUseBiometryUseCase: CanUseBiometryUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val result = MutableStateFlow<HotWalletPasswordRequester.Result?>(null)
|
||||
|
|
@ -60,7 +62,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
|
|||
it.copy(
|
||||
isShown = true,
|
||||
accessCode = "",
|
||||
useBiometricVisible = attemptRequest.hasBiometry,
|
||||
useBiometricVisible = attemptRequest.isBiometryButtonVisible(),
|
||||
onAccessCodeChange = ::onAccessCodeChange,
|
||||
)
|
||||
}
|
||||
|
|
@ -83,7 +85,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
|
|||
it.copy(
|
||||
accessCodeColor = PinTextColor.WrongCode,
|
||||
onAccessCodeChange = {},
|
||||
useBiometricVisible = currentRequest.hasBiometry,
|
||||
useBiometricVisible = currentRequest.isBiometryButtonVisible(),
|
||||
)
|
||||
}
|
||||
delay(timeMillis = 500) // Delay to show the wrong access code state
|
||||
|
|
@ -212,6 +214,9 @@ internal class HotAccessCodeRequestModel @Inject constructor(
|
|||
dismiss()
|
||||
}
|
||||
|
||||
private suspend fun HotWalletPasswordRequester.AttemptRequest.isBiometryButtonVisible(): Boolean =
|
||||
hasBiometry && canUseBiometryUseCase()
|
||||
|
||||
private fun dismissState() {
|
||||
uiState.update {
|
||||
it.copy(isShown = false)
|
||||
|
|
|
|||
|
|
@ -2,21 +2,13 @@ package com.tangem.features.managetokens.component
|
|||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface ManageTokensComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val mode: ManageTokensMode,
|
||||
val source: ManageTokensSource,
|
||||
) {
|
||||
constructor(userWalletId: UserWalletId?, source: ManageTokensSource) : this(
|
||||
source = source,
|
||||
mode = userWalletId
|
||||
?.let { ManageTokensMode.Wallet(userWalletId) }
|
||||
?: ManageTokensMode.None,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, ManageTokensComponent>
|
||||
}
|
||||
|
|
@ -17,6 +17,13 @@ sealed interface ManageTokensMode {
|
|||
}
|
||||
|
||||
sealed interface AddCustomTokenMode {
|
||||
data class Wallet(val userWalletId: UserWalletId) : AddCustomTokenMode
|
||||
|
||||
val userWalletId: UserWalletId
|
||||
get() = when (this) {
|
||||
is Account -> accountId.userWalletId
|
||||
is Wallet -> userWalletId
|
||||
}
|
||||
|
||||
data class Wallet(override val userWalletId: UserWalletId) : AddCustomTokenMode
|
||||
data class Account(val accountId: AccountId) : AddCustomTokenMode
|
||||
}
|
||||
|
|
@ -25,6 +25,8 @@ dependencies {
|
|||
implementation(projects.common.ui)
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.account.status)
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.manageTokens)
|
||||
|
|
@ -35,6 +37,14 @@ dependencies {
|
|||
implementation(projects.domain.swap.models)
|
||||
implementation(projects.domain.notifications)
|
||||
|
||||
// region Project - Libs
|
||||
implementation(projects.libs.crypto)
|
||||
// endregion
|
||||
|
||||
// region Tangem SDKs
|
||||
implementation(tangemDeps.blockchain)
|
||||
// endregion
|
||||
|
||||
/* AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
implementation(deps.lifecycle.compose)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ internal sealed class ManageTokensUM {
|
|||
isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress,
|
||||
scrollToTop: StateEvent<Unit> = this.scrollToTop,
|
||||
needToInteractWithColdWallet: Boolean = this is ManageContent && this.needToInteractWithColdWallet,
|
||||
topBar: ManageTokensTopBarUM? = this.topBar,
|
||||
): ManageTokensUM {
|
||||
return when (this) {
|
||||
is ManageContent -> copy(
|
||||
|
|
@ -65,6 +66,7 @@ internal sealed class ManageTokensUM {
|
|||
isSavingInProgress = isSavingInProgress,
|
||||
scrollToTop = scrollToTop,
|
||||
needToInteractWithColdWallet = needToInteractWithColdWallet,
|
||||
topBar = topBar,
|
||||
)
|
||||
is ReadContent -> copy(
|
||||
search = search,
|
||||
|
|
@ -72,6 +74,7 @@ internal sealed class ManageTokensUM {
|
|||
isInitialBatchLoading = isInitialBatchLoading,
|
||||
isNextBatchLoading = isNextBatchLoading,
|
||||
scrollToTop = scrollToTop,
|
||||
topBar = topBar,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,22 @@ package com.tangem.features.managetokens.model
|
|||
import arrow.core.getOrElse
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.ui.account.toUM
|
||||
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.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
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.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.domain.managetokens.GetSupportedNetworksUseCase
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.features.managetokens.component.AddCustomTokenMode
|
||||
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
|
||||
|
|
@ -25,10 +34,12 @@ import com.tangem.features.managetokens.entity.item.SelectableItemUM
|
|||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.features.managetokens.utils.mapper.toCurrencyNetworkModel
|
||||
import com.tangem.features.managetokens.utils.mapper.toDerivationPathModel
|
||||
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
|
@ -38,6 +49,8 @@ internal class CustomTokenSelectorModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getSupportedNetworksUseCase: GetSupportedNetworksUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -146,9 +159,8 @@ internal class CustomTokenSelectorModel @Inject constructor(
|
|||
return derivationPaths
|
||||
}
|
||||
|
||||
private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List<Network> = when (mode) {
|
||||
is AddCustomTokenMode.Account -> TODO("Account")
|
||||
is AddCustomTokenMode.Wallet -> getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e ->
|
||||
private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List<Network> {
|
||||
return getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e ->
|
||||
val message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error))
|
||||
messageSender.send(message)
|
||||
|
||||
|
|
@ -168,7 +180,60 @@ internal class CustomTokenSelectorModel @Inject constructor(
|
|||
fun selectCustomDerivationPath(value: SelectedDerivationPath) {
|
||||
when (params) {
|
||||
is NetworkSelector -> return
|
||||
is DerivationPathSelector -> params.onDerivationPathSelected(value)
|
||||
is DerivationPathSelector -> if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
params.checkAccountDerivation(value)
|
||||
} else {
|
||||
params.onDerivationPathSelected(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DerivationPathSelector.checkAccountDerivation(derivationPath: SelectedDerivationPath) =
|
||||
modelScope.launch {
|
||||
val accountName = derivationPath.id
|
||||
?.let { Blockchain.fromId(it.rawId.value) }?.let(::AccountNodeRecognizer)
|
||||
?.let { recognizer -> derivationPath.value.value?.let { recognizer.recognize(it) } }
|
||||
?.let { accountNode ->
|
||||
fun AccountStatus.CryptoPortfolio.sameNodeAndNotMain() = !this.account.isMainAccount &&
|
||||
this.account.derivationIndex.value.toLong() == accountNode
|
||||
|
||||
val accounts = singleAccountStatusListSupplier(mode.userWalletId)
|
||||
.first().accountStatuses
|
||||
val account = accounts.find {
|
||||
when (it) {
|
||||
is AccountStatus.CryptoPortfolio -> it.sameNodeAndNotMain()
|
||||
}
|
||||
}
|
||||
val accountName = when (account) {
|
||||
is AccountStatus.CryptoPortfolio -> account.account.accountName.toUM()
|
||||
null -> null
|
||||
}
|
||||
accountName
|
||||
}
|
||||
|
||||
if (accountName == null) {
|
||||
onDerivationPathSelected(derivationPath)
|
||||
} else {
|
||||
showAccountNameExist(
|
||||
accountName = accountName.value,
|
||||
onClick = { onDerivationPathSelected(derivationPath) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAccountNameExist(accountName: TextReference, onClick: () -> Unit) {
|
||||
val firstAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_got_it),
|
||||
onClick = onClick,
|
||||
)
|
||||
val dialogMessage = DialogMessage(
|
||||
title = resourceReference(R.string.custom_token_another_account_dialog_title),
|
||||
message = resourceReference(
|
||||
R.string.custom_token_another_account_dialog_description,
|
||||
wrappedList(accountName),
|
||||
),
|
||||
firstActionBuilder = { firstAction },
|
||||
)
|
||||
messageSender.send(dialogMessage)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import com.tangem.core.ui.event.triggeredEvent
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
|
||||
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
|
|
@ -40,13 +41,14 @@ import kotlinx.coroutines.launch
|
|||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@ModelScoped
|
||||
internal class ManageTokensModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
manageTokensListManagerFactory: ManageTokensListManager.Factory,
|
||||
manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory,
|
||||
paramsContainer: ParamsContainer,
|
||||
|
|
@ -86,6 +88,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
manageTokensListManager.launchPagination(isCollapsed = true)
|
||||
}
|
||||
checkIsSupportAddCustomTokens()
|
||||
}
|
||||
|
||||
fun reloadList() {
|
||||
|
|
@ -105,16 +108,34 @@ internal class ManageTokensModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getTopBarInitialState(): ManageTokensTopBarUM = when (params.mode) {
|
||||
is ManageTokensMode.Wallet -> manageContentTopBar()
|
||||
is ManageTokensMode.Account -> ManageTokensTopBarUM.ReadContent(
|
||||
title = resourceReference(id = R.string.main_manage_tokens),
|
||||
onBackButtonClick = router::pop,
|
||||
)
|
||||
ManageTokensMode.None -> ManageTokensTopBarUM.ReadContent(
|
||||
title = resourceReference(R.string.common_search_tokens),
|
||||
onBackButtonClick = router::pop,
|
||||
)
|
||||
}
|
||||
|
||||
private fun manageContentTopBar() = ManageTokensTopBarUM.ManageContent(
|
||||
title = resourceReference(id = R.string.main_manage_tokens),
|
||||
onBackButtonClick = router::pop,
|
||||
endButton = TopAppBarButtonUM.Icon(
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
onClicked = ::navigateToAddCustomToken,
|
||||
),
|
||||
)
|
||||
|
||||
private fun createReadContentModel(): ManageTokensUM.ReadContent {
|
||||
return ManageTokensUM.ReadContent(
|
||||
popBack = router::pop,
|
||||
isInitialBatchLoading = true,
|
||||
isNextBatchLoading = false,
|
||||
items = getLoadingItems(),
|
||||
topBar = ManageTokensTopBarUM.ReadContent(
|
||||
title = resourceReference(R.string.common_search_tokens),
|
||||
onBackButtonClick = router::pop,
|
||||
),
|
||||
topBar = getTopBarInitialState(),
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.common_search),
|
||||
query = "",
|
||||
|
|
@ -132,14 +153,7 @@ internal class ManageTokensModel @Inject constructor(
|
|||
isInitialBatchLoading = true,
|
||||
isNextBatchLoading = false,
|
||||
items = getLoadingItems(),
|
||||
topBar = ManageTokensTopBarUM.ManageContent(
|
||||
title = resourceReference(id = R.string.main_manage_tokens),
|
||||
onBackButtonClick = router::pop,
|
||||
endButton = TopAppBarButtonUM.Icon(
|
||||
iconRes = R.drawable.ic_plus_24,
|
||||
onClicked = ::navigateToAddCustomToken,
|
||||
),
|
||||
),
|
||||
topBar = getTopBarInitialState(),
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.common_search),
|
||||
query = "",
|
||||
|
|
@ -174,6 +188,20 @@ internal class ManageTokensModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun checkIsSupportAddCustomTokens() {
|
||||
when (val mode = params.mode) {
|
||||
is ManageTokensMode.Account -> modelScope.launch {
|
||||
val mainAccount = singleAccountStatusListSupplier(mode.accountId.userWalletId).first().mainAccount
|
||||
if (mode.accountId == mainAccount.account.accountId) {
|
||||
state.update { it.copySealed(topBar = manageContentTopBar()) }
|
||||
}
|
||||
}
|
||||
ManageTokensMode.None,
|
||||
is ManageTokensMode.Wallet,
|
||||
-> Unit // use init state
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateItems(items: ImmutableList<CurrencyItemUM>) {
|
||||
val updatedState = state.updateAndGet { state ->
|
||||
state.copySealed(
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import com.tangem.core.ui.utils.WindowInsetsZero
|
|||
import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.managetokens.component.ManageTokensMode
|
||||
import com.tangem.features.managetokens.component.ManageTokensSource
|
||||
import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent
|
||||
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
|
||||
|
|
@ -442,20 +443,23 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider<Ma
|
|||
showTangemIcon = true,
|
||||
params = ManageTokensComponent.Params(
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
userWalletId = UserWalletId("0x"),
|
||||
mode = ManageTokensMode.Wallet(UserWalletId("0x")),
|
||||
),
|
||||
),
|
||||
PreviewManageTokensComponent(
|
||||
isLoading = false,
|
||||
showTangemIcon = true,
|
||||
params = ManageTokensComponent.Params(source = ManageTokensSource.ONBOARDING, userWalletId = null),
|
||||
params = ManageTokensComponent.Params(
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
mode = ManageTokensMode.None,
|
||||
),
|
||||
),
|
||||
PreviewManageTokensComponent(
|
||||
isLoading = false,
|
||||
showTangemIcon = false,
|
||||
params = ManageTokensComponent.Params(
|
||||
source = ManageTokensSource.ONBOARDING,
|
||||
userWalletId = UserWalletId("0x"),
|
||||
mode = ManageTokensMode.Wallet(UserWalletId("0x")),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,11 +18,14 @@ dependencies {
|
|||
api(projects.features.sendV2.api)
|
||||
api(projects.features.tokenRecieve.api)
|
||||
api(projects.features.wallet.api)
|
||||
api(projects.features.account.api)
|
||||
|
||||
/* Data */
|
||||
implementation(projects.data.common)
|
||||
|
||||
/* Domain */
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.account.status)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.markets.portfolio.add.api
|
||||
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
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
|
||||
|
||||
internal data class AvailableToAddData(
|
||||
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet>,
|
||||
) {
|
||||
val availableToAdd: Boolean
|
||||
get() = availableToAddWallets.isNotEmpty()
|
||||
val isSinglePortfolio: Boolean
|
||||
get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1
|
||||
}
|
||||
|
||||
internal data class AvailableToAddWallet(
|
||||
val userWallet: UserWallet,
|
||||
val accounts: Set<AccountStatus>,
|
||||
val availableNetworks: Set<TokenMarketInfo.Network>,
|
||||
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount>,
|
||||
)
|
||||
|
||||
internal data class AvailableToAddAccount(
|
||||
val account: AccountStatus,
|
||||
val availableNetworks: Set<TokenMarketInfo.Network>,
|
||||
val addedNetworks: Set<Network>,
|
||||
) {
|
||||
val isSingleNetwork: Boolean
|
||||
get() = availableNetworks.size == 1
|
||||
|
||||
val availableToAddNetworks: Set<TokenMarketInfo.Network> = availableNetworks
|
||||
.filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } }
|
||||
.toSet()
|
||||
|
||||
val addedMarketNetworks: Set<TokenMarketInfo.Network> = availableNetworks
|
||||
.filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
internal data class SelectedPortfolio(
|
||||
val userWallet: UserWallet,
|
||||
val account: AvailableToAddAccount,
|
||||
val isAccountMode: Boolean,
|
||||
val availableMorePortfolio: Boolean,
|
||||
)
|
||||
|
||||
internal data class SelectedNetwork(
|
||||
val selectedNetwork: TokenMarketInfo.Network,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val availableMoreNetwork: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
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.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
|
||||
import com.tangem.features.markets.portfolio.add.impl.model.AddTokenModel
|
||||
import com.tangem.features.markets.portfolio.add.impl.ui.AddTokenContent
|
||||
import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class AddTokenComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : AppComponentContext by context, ComposableContentComponent {
|
||||
|
||||
private val model: AddTokenModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state = model.uiState.collectAsStateWithLifecycle()
|
||||
val um = state.value ?: return
|
||||
AddTokenContent(
|
||||
modifier = modifier,
|
||||
state = um,
|
||||
)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val marketParams: TokenMarketParams,
|
||||
val eventBuilder: PortfolioAnalyticsEvent.EventBuilder,
|
||||
val selectedPortfolio: Flow<SelectedPortfolio>,
|
||||
val selectedNetwork: Flow<SelectedNetwork>,
|
||||
val callbacks: Callbacks,
|
||||
)
|
||||
|
||||
interface Callbacks {
|
||||
fun onChangeNetworkClick()
|
||||
fun onChangePortfolioClick()
|
||||
fun onTokenAdded(status: CryptoCurrencyStatus)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : ComponentFactory<Params, AddTokenComponent> {
|
||||
override fun create(context: AppComponentContext, params: Params): AddTokenComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
ChooseNetworkContent(state)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val alreadyAdded: Set<TokenMarketInfo.Network>,
|
||||
val allAvailable: List<TokenMarketInfo.Network>,
|
||||
val callbacks: Callbacks,
|
||||
)
|
||||
|
||||
interface Callbacks {
|
||||
fun onNetworkSelected(network: TokenMarketInfo.Network)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : ComponentFactory<Params, ChooseNetworkComponent> {
|
||||
override fun create(context: AppComponentContext, params: Params): ChooseNetworkComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
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.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.features.markets.portfolio.add.impl.model.TokenActionsModel
|
||||
import com.tangem.features.markets.portfolio.add.impl.ui.TokenActionsContent
|
||||
import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent
|
||||
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class TokenActionsComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
) : AppComponentContext by context, ComposableContentComponent {
|
||||
|
||||
private val model: TokenActionsModel = getOrCreateModel(params)
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = TokenReceiveConfig.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state = model.uiState.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
val tokenActionsUM = state.value ?: return
|
||||
TokenActionsContent(
|
||||
modifier = modifier,
|
||||
state = tokenActionsUM,
|
||||
)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
private fun bottomSheetChild(
|
||||
config: TokenReceiveConfig,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = TokenReceiveComponent.Params(
|
||||
config = config,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
|
||||
data class Params(
|
||||
val eventBuilder: PortfolioAnalyticsEvent.EventBuilder,
|
||||
val data: Flow<PortfolioData.CryptoCurrencyData>,
|
||||
val callbacks: Callbacks,
|
||||
)
|
||||
|
||||
interface Callbacks {
|
||||
fun onLaterClick()
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : ComponentFactory<Params, TokenActionsComponent> {
|
||||
override fun create(context: AppComponentContext, params: Params): TokenActionsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.converter
|
||||
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
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.AccountId
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.account.PortfolioFetcher
|
||||
import com.tangem.features.markets.portfolio.add.api.AvailableToAddAccount
|
||||
import com.tangem.features.markets.portfolio.add.api.AvailableToAddData
|
||||
import com.tangem.features.markets.portfolio.add.api.AvailableToAddWallet
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AvailableToAddDataConverter @Inject constructor(
|
||||
private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase,
|
||||
private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
) {
|
||||
|
||||
suspend fun convert(
|
||||
balances: Map<UserWallet, PortfolioFetcher.PortfolioBalance>,
|
||||
availableNetworks: Set<TokenMarketInfo.Network>,
|
||||
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()
|
||||
return AvailableToAddAccount(
|
||||
account = this,
|
||||
availableNetworks = availableNetworks,
|
||||
addedNetworks = addedNetworks,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getAvailableToAddWallet(
|
||||
entry: Map.Entry<UserWallet, PortfolioFetcher.PortfolioBalance>,
|
||||
): AvailableToAddWallet {
|
||||
val (wallet, balance) = entry
|
||||
val filteredNetworks = wallet.filteredAvailableNetworks(availableNetworks)
|
||||
val accounts = balance.accountsBalance.accountStatuses
|
||||
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount> = accounts
|
||||
.map { it.account.accountId to it.getAvailableToAddAccount(wallet) }
|
||||
.filter { (_, account) -> account.availableToAddNetworks.isNotEmpty() }
|
||||
.toMap()
|
||||
return AvailableToAddWallet(
|
||||
userWallet = wallet,
|
||||
accounts = accounts,
|
||||
availableNetworks = filteredNetworks,
|
||||
availableToAddAccounts = availableToAddAccounts,
|
||||
)
|
||||
}
|
||||
|
||||
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet> = balances
|
||||
.map {
|
||||
val (wallet, balance) = it
|
||||
val availableToAddWallet = getAvailableToAddWallet(it)
|
||||
wallet.walletId to availableToAddWallet
|
||||
}
|
||||
.filter { (_, wallet) -> wallet.availableToAddAccounts.isNotEmpty() }
|
||||
.toMap()
|
||||
|
||||
return AvailableToAddData(
|
||||
availableToAddWallets = availableToAddWallets,
|
||||
)
|
||||
}
|
||||
|
||||
private fun UserWallet.filteredAvailableNetworks(networks: Set<TokenMarketInfo.Network>) =
|
||||
filterAvailableNetworksForWalletUseCase(
|
||||
userWalletId = this.walletId,
|
||||
networks = networks,
|
||||
)
|
||||
|
||||
private suspend fun createCryptoCurrency(
|
||||
userWallet: UserWallet,
|
||||
network: TokenMarketInfo.Network,
|
||||
marketParams: TokenMarketParams,
|
||||
): CryptoCurrency? = getTokenMarketCryptoCurrency(
|
||||
userWalletId = userWallet.walletId,
|
||||
tokenMarketParams = marketParams,
|
||||
network = network,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.model
|
||||
|
||||
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.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.SaveCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
|
||||
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
|
||||
import com.tangem.features.markets.portfolio.add.impl.model.AddTokenUiBuilder.Companion.toggleProgress
|
||||
import com.tangem.features.markets.portfolio.add.impl.ui.state.AddTokenUM
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
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 javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class AddTokenModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val uiBuilder: AddTokenUiBuilder,
|
||||
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val saveCryptoCurrenciesUseCase: SaveCryptoCurrenciesUseCase,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<AddTokenComponent.Params>()
|
||||
private val analyticsEventBuilder = params.eventBuilder
|
||||
private val addTokenJob = JobHolder()
|
||||
|
||||
val uiState: StateFlow<AddTokenUM?>
|
||||
field = MutableStateFlow(value = null)
|
||||
|
||||
init {
|
||||
combine(
|
||||
flow = params.selectedNetwork.distinctUntilChanged(),
|
||||
flow2 = params.selectedPortfolio.distinctUntilChanged(),
|
||||
transform = { selectedNetwork, selectedPortfolio ->
|
||||
addTokenJob.cancel()
|
||||
val isTangemIconVisible = needColdWalletInteraction(selectedNetwork, selectedPortfolio)
|
||||
uiBuilder.updateContent(
|
||||
selectedPortfolio = selectedPortfolio,
|
||||
selectedNetwork = selectedNetwork,
|
||||
isTangemIconVisible = isTangemIconVisible,
|
||||
onConfirmClick = { onAddClick(selectedNetwork, selectedPortfolio).saveIn(addTokenJob) },
|
||||
)
|
||||
},
|
||||
)
|
||||
.onEach { newUI -> uiState.value = newUI }
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onAddClick(selectedNetwork: SelectedNetwork, selectedPortfolio: SelectedPortfolio) =
|
||||
modelScope.launch(dispatchers.default) {
|
||||
val um = uiState.value ?: return@launch
|
||||
uiState.value = um.toggleProgress(true)
|
||||
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(),
|
||||
)
|
||||
val status = getAccountCurrencyStatusUseCase.invokeSync(
|
||||
userWalletId = accountId.userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
network = cryptoCurrency.network,
|
||||
).getOrNull() ?: return@launch
|
||||
params.callbacks.onTokenAdded(status.status)
|
||||
uiState.value = um.toggleProgress(false)
|
||||
}
|
||||
|
||||
private suspend fun needColdWalletInteraction(
|
||||
selectedNetwork: SelectedNetwork,
|
||||
selectedPortfolio: SelectedPortfolio,
|
||||
): Boolean = coldWalletAndHasMissedDerivationsUseCase.invoke(
|
||||
userWalletId = selectedPortfolio.userWallet.walletId,
|
||||
networksWithDerivationPath = mapOf(selectedNetwork.selectedNetwork.networkId to null),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.model
|
||||
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconUM
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.iconResId
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
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
|
||||
import com.tangem.features.markets.portfolio.add.impl.ui.state.AddTokenUM
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class AddTokenUiBuilder @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
) {
|
||||
private val params = paramsContainer.require<AddTokenComponent.Params>()
|
||||
|
||||
private fun createNetwork(selectedNetwork: SelectedNetwork): AddTokenUM.Network {
|
||||
return AddTokenUM.Network(
|
||||
icon = selectedNetwork.cryptoCurrency.network.iconResId,
|
||||
name = stringReference(selectedNetwork.cryptoCurrency.network.name),
|
||||
editable = selectedNetwork.availableMoreNetwork,
|
||||
onClick = { params.callbacks.onChangeNetworkClick() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun createPortfolio(selectedPortfolio: SelectedPortfolio): AddTokenUM.Portfolio {
|
||||
val accountIcon: CryptoPortfolioIconUM?
|
||||
val portfolioName: TextReference
|
||||
when (selectedPortfolio.isAccountMode) {
|
||||
false -> {
|
||||
accountIcon = null
|
||||
portfolioName = stringReference(selectedPortfolio.userWallet.name)
|
||||
}
|
||||
true -> {
|
||||
val accountStatus = selectedPortfolio.account.account
|
||||
portfolioName = accountStatus.account.accountName.toUM().value
|
||||
accountIcon = when (accountStatus) {
|
||||
is AccountStatus.CryptoPortfolio -> accountStatus.account.icon.toUM()
|
||||
}
|
||||
}
|
||||
}
|
||||
return AddTokenUM.Portfolio(
|
||||
accountIconUM = accountIcon,
|
||||
name = portfolioName,
|
||||
editable = selectedPortfolio.availableMorePortfolio,
|
||||
onClick = { params.callbacks.onChangePortfolioClick() },
|
||||
)
|
||||
}
|
||||
|
||||
fun updateContent(
|
||||
selectedPortfolio: SelectedPortfolio,
|
||||
selectedNetwork: SelectedNetwork,
|
||||
isTangemIconVisible: Boolean,
|
||||
onConfirmClick: () -> Unit,
|
||||
): AddTokenUM {
|
||||
// its may happens when change portfolio after selected both params in line navigation
|
||||
val isAvailableNetwork = selectedPortfolio.account.availableToAddNetworks
|
||||
.any { selectedNetwork.selectedNetwork.networkId == it.networkId }
|
||||
val button = AddTokenUM.Button(
|
||||
isEnabled = isAvailableNetwork,
|
||||
showProgress = false,
|
||||
isTangemIconVisible = isTangemIconVisible,
|
||||
text = resourceReference(R.string.common_add),
|
||||
onConfirmClick = onConfirmClick,
|
||||
)
|
||||
val networkUM = createNetwork(selectedNetwork)
|
||||
val portfolioUM = createPortfolio(selectedPortfolio)
|
||||
val currency = selectedNetwork.cryptoCurrency
|
||||
val tokenToAdd = TokenItemState.Content(
|
||||
id = currency.id.value,
|
||||
iconState = CryptoCurrencyToIconStateConverter().convert(currency),
|
||||
titleState = TokenItemState.TitleState.Content(stringReference(currency.name)),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(text = ""),
|
||||
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = ""),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(currency.symbol)),
|
||||
onItemClick = null,
|
||||
onItemLongClick = null,
|
||||
)
|
||||
return AddTokenUM(
|
||||
tokenToAdd = tokenToAdd,
|
||||
network = networkUM,
|
||||
portfolio = portfolioUM,
|
||||
button = button,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun AddTokenUM.toggleProgress(showProgress: Boolean) = this.copy(
|
||||
button = this.button.copy(showProgress = showProgress),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.model
|
||||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
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.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory
|
||||
import com.tangem.features.markets.portfolio.add.impl.TokenActionsComponent
|
||||
import com.tangem.features.markets.portfolio.add.impl.ui.state.TokenActionsUM
|
||||
import com.tangem.features.markets.portfolio.impl.model.TokenActionsHandler
|
||||
import com.tangem.features.markets.portfolio.impl.model.TokenActionsHandler.HandledQuickAction
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class TokenActionsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
tokenActionsIntentsFactory: TokenActionsHandler.Factory,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val uiBuilder: TokenActionsUiBuilder,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val receiveAddressesFactory: ReceiveAddressesFactory,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<TokenActionsComponent.Params>()
|
||||
private val analyticsEventBuilder get() = params.eventBuilder
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase.invokeOrDefault()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
|
||||
private val tokenActionsHandler: TokenActionsHandler =
|
||||
tokenActionsIntentsFactory.create(
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
updateTokenReceiveBSConfig = { },
|
||||
onHandleQuickAction = { handledAction -> handledQuickAction(handledAction) },
|
||||
)
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = SlotNavigation()
|
||||
val uiState: StateFlow<TokenActionsUM?> = params.data
|
||||
.mapLatest { uiBuilder.build(it, tokenActionsHandler) }
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
private fun handledQuickAction(handledAction: HandledQuickAction) {
|
||||
val event = analyticsEventBuilder.quickActionClick(
|
||||
actionUM = handledAction.action,
|
||||
blockchainName = handledAction.cryptoCurrencyData.status.currency.network.name,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
val isReceive = handledAction.action == TokenActionsBSContentUM.Action.Receive
|
||||
if (!isReceive) return
|
||||
modelScope.launch {
|
||||
val tokenConfig = receiveAddressesFactory.create(
|
||||
status = handledAction.cryptoCurrencyData.status,
|
||||
userWalletId = handledAction.cryptoCurrencyData.userWallet.walletId,
|
||||
) ?: return@launch
|
||||
bottomSheetNavigation.activate(tokenConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.model
|
||||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.markets.portfolio.add.impl.TokenActionsComponent
|
||||
import com.tangem.features.markets.portfolio.add.impl.ui.state.TokenActionsUM
|
||||
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.markets.portfolio.impl.model.PortfolioTokenUMConverter
|
||||
import com.tangem.features.markets.portfolio.impl.model.TokenActionsHandler
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class TokenActionsUiBuilder @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
) {
|
||||
private val params = paramsContainer.require<TokenActionsComponent.Params>()
|
||||
|
||||
fun build(data: PortfolioData.CryptoCurrencyData, tokenActionsHandler: TokenActionsHandler): TokenActionsUM {
|
||||
val status = data.status
|
||||
val tokenUM = TokenItemState.Content(
|
||||
id = status.currency.id.value,
|
||||
iconState = CryptoCurrencyToIconStateConverter().convert(status.currency),
|
||||
titleState = TokenItemState.TitleState.Content(stringReference(status.currency.name)),
|
||||
fiatAmountState = null,
|
||||
subtitle2State = null,
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(status.currency.symbol)),
|
||||
onItemClick = null,
|
||||
onItemLongClick = null,
|
||||
)
|
||||
return TokenActionsUM(
|
||||
token = tokenUM,
|
||||
onLaterClick = { params.callbacks.onLaterClick() },
|
||||
quickActions = PortfolioTokenUMConverter.quickActions(data, tokenActionsHandler),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
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.HorizontalDivider
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
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.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.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
|
||||
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.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.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.WalletConnectBottomSheetTestTags
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.portfolio.add.impl.ui.state.AddTokenUM
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
internal fun AddTokenContent(state: AddTokenUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
TokenItem(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
|
||||
.background(color = TangemTheme.colors.background.action),
|
||||
state = state.tokenToAdd,
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
|
||||
SpacerH(TangemTheme.dimens.spacing14)
|
||||
Column(
|
||||
modifier = Modifier.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius14),
|
||||
),
|
||||
) {
|
||||
PortfolioRow(state.portfolio)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
thickness = TangemTheme.dimens.size0_5,
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
)
|
||||
NetworkRow(state.network)
|
||||
}
|
||||
|
||||
SpacerH16()
|
||||
|
||||
AddButton(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
state = state.button,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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(
|
||||
modifier = modifier
|
||||
.clickable(enabled = state.editable, onClick = state.onClick)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResourceSafe(R.string.wc_common_networks),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerW12()
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = Color.Unspecified,
|
||||
imageVector = ImageVector.vectorResource(id = state.icon),
|
||||
contentDescription = null,
|
||||
)
|
||||
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 AddButton(state: AddTokenUM.Button, modifier: Modifier = Modifier) {
|
||||
val endIcon = if (state.isEnabled && state.isTangemIconVisible) {
|
||||
TangemButtonIconPosition.End(R.drawable.ic_tangem_24)
|
||||
} else {
|
||||
TangemButtonIconPosition.None
|
||||
}
|
||||
PrimaryButtonIconEnd(
|
||||
modifier = modifier,
|
||||
text = state.text.resolveReference(),
|
||||
iconResId = endIcon.iconResId,
|
||||
onClick = state.onConfirmClick,
|
||||
enabled = state.isEnabled,
|
||||
showProgress = state.showProgress,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview(@PreviewParameter(PreviewProvider::class) state: AddTokenUM) {
|
||||
TangemThemePreview {
|
||||
AddTokenContent(
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class PreviewProvider : PreviewParameterProvider<AddTokenUM> {
|
||||
private val tokenState
|
||||
get() = TokenItemState.Content(
|
||||
id = UUID.randomUUID().toString(),
|
||||
iconState = CurrencyIconState.TokenIcon(
|
||||
url = null,
|
||||
topBadgeIconResId = R.drawable.img_eth_22,
|
||||
fallbackTint = TangemColorPalette.Black,
|
||||
fallbackBackground = TangemColorPalette.Meadow,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
titleState = TokenItemState.TitleState.Content(
|
||||
text = stringReference(value = "Tether"),
|
||||
),
|
||||
fiatAmountState = FiatAmountState.Content(text = ""),
|
||||
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = ""),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference("USDT")),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
)
|
||||
|
||||
val networkUM
|
||||
get() = AddTokenUM.Network(
|
||||
icon = R.drawable.img_eth_22,
|
||||
name = stringReference("Ethereum"),
|
||||
editable = true,
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
val button
|
||||
get() = AddTokenUM.Button(
|
||||
isEnabled = false,
|
||||
showProgress = false,
|
||||
isTangemIconVisible = false,
|
||||
text = resourceReference(R.string.common_add),
|
||||
onConfirmClick = { },
|
||||
)
|
||||
|
||||
val account
|
||||
get() = AddTokenUM.Portfolio(
|
||||
accountIconUM = AccountIconPreviewData.randomAccountIcon(),
|
||||
name = AccountName.DefaultMain.toUM().value,
|
||||
editable = true,
|
||||
onClick = {},
|
||||
)
|
||||
val wallet
|
||||
get() = AddTokenUM.Portfolio(
|
||||
accountIconUM = null,
|
||||
name = stringReference("Wallet"),
|
||||
editable = true,
|
||||
onClick = {},
|
||||
)
|
||||
|
||||
override val values: Sequence<AddTokenUM>
|
||||
get() = sequenceOf(
|
||||
AddTokenUM(
|
||||
tokenToAdd = tokenState,
|
||||
network = networkUM,
|
||||
portfolio = account,
|
||||
button = button,
|
||||
),
|
||||
AddTokenUM(
|
||||
tokenToAdd = tokenState,
|
||||
network = networkUM,
|
||||
portfolio = wallet,
|
||||
button = button,
|
||||
),
|
||||
AddTokenUM(
|
||||
tokenToAdd = tokenState,
|
||||
network = networkUM.copy(editable = false),
|
||||
portfolio = wallet.copy(editable = false),
|
||||
button = button.copy(
|
||||
isEnabled = true,
|
||||
isTangemIconVisible = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.util.fastForEachIndexed
|
||||
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.BlockchainRow
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.add.impl.ui.state.ChooseNetworkUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.util.UUID
|
||||
|
||||
private const val DISABLED_ALPHA = 0.4f
|
||||
|
||||
@Composable
|
||||
internal fun ChooseNetworkContent(state: ChooseNetworkUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
state.networks.fastForEachIndexed { index, model ->
|
||||
key(model.id) {
|
||||
BlockchainRow(
|
||||
model = model,
|
||||
itemPadding = PaddingValues(
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
vertical = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = model.isEnabled, onClick = { state.onNetworkClick(model) }),
|
||||
) {
|
||||
if (!model.isEnabled) {
|
||||
Label(
|
||||
modifier = Modifier.alpha(DISABLED_ALPHA),
|
||||
state = LabelUM(
|
||||
text = resourceReference(R.string.common_added),
|
||||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview(@PreviewParameter(ChooseNetworkContentProvider::class) content: ChooseNetworkUM) {
|
||||
TangemThemePreview {
|
||||
ChooseNetworkContent(
|
||||
state = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal class ChooseNetworkContentProvider : PreviewParameterProvider<ChooseNetworkUM> {
|
||||
|
||||
private val blockchainRow = BlockchainRowUM(
|
||||
id = UUID.randomUUID().toString(),
|
||||
name = "Etherium 3",
|
||||
type = "TEST",
|
||||
iconResId = R.drawable.img_eth_22,
|
||||
isMainNetwork = false,
|
||||
isSelected = true,
|
||||
isEnabled = true,
|
||||
)
|
||||
|
||||
override val values: Sequence<ChooseNetworkUM>
|
||||
get() = sequenceOf(
|
||||
ChooseNetworkUM(
|
||||
onNetworkClick = {},
|
||||
networks = persistentListOf(
|
||||
blockchainRow.copy(
|
||||
type = "MAIN",
|
||||
isMainNetwork = true,
|
||||
),
|
||||
blockchainRow.copy(
|
||||
iconResId = R.drawable.ic_bsc_16,
|
||||
isEnabled = false,
|
||||
),
|
||||
blockchainRow.copy(iconResId = R.drawable.img_polygon_22),
|
||||
blockchainRow.copy(iconResId = R.drawable.img_optimism_22),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
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.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
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 androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.icons.badge.drawBadge
|
||||
import com.tangem.core.ui.components.token.TokenItem
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||
import com.tangem.core.ui.res.LocalHapticManager
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
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.add.impl.ui.state.TokenActionsUM
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
internal fun TokenActionsContent(state: TokenActionsUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
TokenItem(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
|
||||
.background(color = TangemTheme.colors.background.action),
|
||||
state = state.token,
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
|
||||
SpacerH(TangemTheme.dimens.spacing14)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
state.quickActions.actions.fastForEach {
|
||||
key(it.title) {
|
||||
ActionRow(
|
||||
state = it,
|
||||
onClick = { state.quickActions.onQuickActionClick(it) },
|
||||
onLongClick = { state.quickActions.onQuickActionLongClick(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH16()
|
||||
|
||||
SecondaryButton(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.common_later),
|
||||
onClick = state.onLaterClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun ActionRow(
|
||||
state: QuickActionUM,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: (() -> Unit),
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hapticManager = LocalHapticManager.current
|
||||
val onLongClickInternal = {
|
||||
hapticManager.perform(TangemHapticEffect.View.LongPress)
|
||||
onLongClick()
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onLongClick = onLongClickInternal.takeIf { state.longClickAvailable },
|
||||
onClick = {
|
||||
hapticManager.perform(TangemHapticEffect.View.SegmentTick)
|
||||
onClick()
|
||||
},
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing15),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
val containerColor = TangemTheme.colors.background.action
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
|
||||
shape = CircleShape,
|
||||
)
|
||||
.size(36.dp)
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
if (state is QuickActionUM.Exchange && state.showBadge) {
|
||||
drawBadge(containerColor = containerColor, offset = 4.dp)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.requiredSize(TangemTheme.dimens.size16),
|
||||
imageVector = ImageVector.vectorResource(id = state.icon),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
|
||||
) {
|
||||
Text(
|
||||
text = state.title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = state.description.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview(@PreviewParameter(TokenActionsContentPreviewProvider::class) state: TokenActionsUM) {
|
||||
TangemThemePreview {
|
||||
TokenActionsContent(
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenActionsContentPreviewProvider : PreviewParameterProvider<TokenActionsUM> {
|
||||
private val tokenState
|
||||
get() = TokenItemState.Content(
|
||||
id = UUID.randomUUID().toString(),
|
||||
iconState = CurrencyIconState.TokenIcon(
|
||||
url = null,
|
||||
topBadgeIconResId = R.drawable.img_eth_22,
|
||||
fallbackTint = TangemColorPalette.Black,
|
||||
fallbackBackground = TangemColorPalette.Meadow,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
titleState = TokenItemState.TitleState.Content(
|
||||
text = stringReference(value = "Tether"),
|
||||
),
|
||||
fiatAmountState = null,
|
||||
subtitle2State = null,
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference("USDT")),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
)
|
||||
|
||||
override val values: Sequence<TokenActionsUM>
|
||||
get() = sequenceOf(
|
||||
TokenActionsUM(
|
||||
quickActions = PortfolioTokenUM.QuickActions(
|
||||
actions = persistentListOf(
|
||||
QuickActionUM.Buy,
|
||||
QuickActionUM.Exchange(showBadge = true),
|
||||
QuickActionUM.Receive,
|
||||
),
|
||||
onQuickActionClick = {},
|
||||
onQuickActionLongClick = {},
|
||||
),
|
||||
token = tokenState,
|
||||
onLaterClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.ui.state
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconUM
|
||||
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 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,
|
||||
val name: TextReference,
|
||||
val editable: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
||||
data class Button(
|
||||
val isEnabled: Boolean,
|
||||
val showProgress: Boolean,
|
||||
val isTangemIconVisible: Boolean,
|
||||
val onConfirmClick: () -> Unit,
|
||||
val text: TextReference,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class ChooseNetworkUM(
|
||||
val networks: ImmutableList<BlockchainRowUM>,
|
||||
val onNetworkClick: (BlockchainRowUM) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.markets.portfolio.add.impl.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM
|
||||
|
||||
internal data class TokenActionsUM(
|
||||
val token: TokenItemState,
|
||||
val quickActions: PortfolioTokenUM.QuickActions,
|
||||
val onLaterClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -2,9 +2,9 @@ package com.tangem.features.markets.portfolio.impl.model
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -22,16 +22,13 @@ 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.ReceiveAddressModel
|
||||
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.network.NetworkAddress
|
||||
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.GetViewedTokenReceiveWarningUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetEnsNameUseCase
|
||||
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
|
||||
|
|
@ -70,9 +67,8 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
private val addToPortfolioManager: AddToPortfolioManager,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
|
||||
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
|
||||
private val getEnsNameUseCase: GetEnsNameUseCase,
|
||||
private val userWalletImageFetcher: UserWalletImageFetcher,
|
||||
private val receiveAddressesFactory: ReceiveAddressesFactory,
|
||||
) : Model() {
|
||||
|
||||
val state: StateFlow<MyPortfolioUM> get() = _state
|
||||
|
|
@ -374,50 +370,16 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) {
|
||||
when (quickAction.action) {
|
||||
TokenActionsBSContentUM.Action.Receive -> {
|
||||
val addresses = quickAction.cryptoCurrencyData.status.value.networkAddress ?: return
|
||||
val cryptoCurrency = quickAction.cryptoCurrencyData.status.currency
|
||||
modelScope.launch {
|
||||
val ensName = getEnsNameUseCase.invoke(
|
||||
userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId,
|
||||
network = cryptoCurrency.network,
|
||||
address = addresses.defaultAddress.value,
|
||||
)
|
||||
|
||||
val receiveAddresses = buildList {
|
||||
ensName?.let { ens ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Ens,
|
||||
value = ens,
|
||||
),
|
||||
)
|
||||
}
|
||||
addresses.availableAddresses.map { address ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = when (address.type) {
|
||||
NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.NameService.Default
|
||||
NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.NameService.Legacy
|
||||
},
|
||||
value = address.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
val tokenConfig = TokenReceiveConfig(
|
||||
shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(),
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId,
|
||||
showMemoDisclaimer = cryptoCurrency.network.transactionExtrasType != Network
|
||||
.TransactionExtrasType.NONE,
|
||||
receiveAddress = receiveAddresses,
|
||||
)
|
||||
bottomSheetNavigation.activate(tokenConfig)
|
||||
}
|
||||
val isNewReceive = quickAction.action == TokenActionsBSContentUM.Action.Receive &&
|
||||
tokenReceiveFeatureToggle.isNewTokenReceiveEnabled
|
||||
if (isNewReceive) {
|
||||
modelScope.launch {
|
||||
val tokenConfig = receiveAddressesFactory.create(
|
||||
status = quickAction.cryptoCurrencyData.status,
|
||||
userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId,
|
||||
) ?: return@launch
|
||||
bottomSheetNavigation.activate(tokenConfig)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,55 +42,60 @@ internal class PortfolioTokenUMConverter(
|
|||
walletId = value.userWallet.walletId,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
isQuickActionsShown = false,
|
||||
quickActions = quickActions(cryptoData = value),
|
||||
quickActions = quickActions(cryptoData = value, tokenActionsHandler = tokenActionsHandler),
|
||||
)
|
||||
}
|
||||
|
||||
private fun quickActions(cryptoData: PortfolioData.CryptoCurrencyData): PortfolioTokenUM.QuickActions {
|
||||
return PortfolioTokenUM.QuickActions(
|
||||
actions = toQuickActions(cryptoData.actions),
|
||||
onQuickActionClick = {
|
||||
when (it) {
|
||||
QuickActionUM.Buy -> tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.Buy,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
is QuickActionUM.Exchange -> tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.Exchange,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
QuickActionUM.Receive -> tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.Receive,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
QuickActionUM.Stake -> tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.Stake,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
}
|
||||
},
|
||||
onQuickActionLongClick = {
|
||||
if (it == QuickActionUM.Receive) {
|
||||
tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.CopyAddress,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun toQuickActions(actions: List<TokenActionsState.ActionState>) = buildList {
|
||||
actions.forEach { action ->
|
||||
if (action.unavailabilityReason == ScenarioUnavailabilityReason.None) {
|
||||
when (action) {
|
||||
is TokenActionsState.ActionState.Buy -> QuickActionUM.Buy
|
||||
is TokenActionsState.ActionState.Swap -> QuickActionUM.Exchange(showBadge = action.showBadge)
|
||||
is TokenActionsState.ActionState.Receive -> QuickActionUM.Receive
|
||||
is TokenActionsState.ActionState.Stake -> QuickActionUM.Stake
|
||||
else -> null
|
||||
}?.let(::add)
|
||||
}
|
||||
companion object {
|
||||
fun quickActions(
|
||||
cryptoData: PortfolioData.CryptoCurrencyData,
|
||||
tokenActionsHandler: TokenActionsHandler,
|
||||
): PortfolioTokenUM.QuickActions {
|
||||
return PortfolioTokenUM.QuickActions(
|
||||
actions = toQuickActions(cryptoData.actions),
|
||||
onQuickActionClick = {
|
||||
when (it) {
|
||||
QuickActionUM.Buy -> tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.Buy,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
is QuickActionUM.Exchange -> tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.Exchange,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
QuickActionUM.Receive -> tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.Receive,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
QuickActionUM.Stake -> tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.Stake,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
}
|
||||
},
|
||||
onQuickActionLongClick = {
|
||||
if (it == QuickActionUM.Receive) {
|
||||
tokenActionsHandler.handle(
|
||||
action = TokenActionsBSContentUM.Action.CopyAddress,
|
||||
cryptoCurrencyData = cryptoData,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}.toImmutableList()
|
||||
|
||||
private fun toQuickActions(actions: List<TokenActionsState.ActionState>) = buildList {
|
||||
actions.forEach { action ->
|
||||
if (action.unavailabilityReason == ScenarioUnavailabilityReason.None) {
|
||||
when (action) {
|
||||
is TokenActionsState.ActionState.Buy -> QuickActionUM.Buy
|
||||
is TokenActionsState.ActionState.Swap -> QuickActionUM.Exchange(showBadge = action.showBadge)
|
||||
is TokenActionsState.ActionState.Receive -> QuickActionUM.Receive
|
||||
is TokenActionsState.ActionState.Stake -> QuickActionUM.Stake
|
||||
else -> null
|
||||
}?.let(::add)
|
||||
}
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ dependencies {
|
|||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.nft.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.account)
|
||||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.nft.component
|
|||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
|
||||
|
|
@ -12,7 +13,9 @@ interface NFTDetailsBlockComponent : ComposableContentComponent {
|
|||
val userWalletId: UserWalletId,
|
||||
val nftAsset: NFTAsset,
|
||||
val nftCollectionName: String,
|
||||
val title: TextReference,
|
||||
val account: Account.CryptoPortfolio?,
|
||||
val isAccountsMode: Boolean,
|
||||
val walletTitle: TextReference,
|
||||
val isSuccessScreen: Boolean,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,15 @@ package com.tangem.features.nft.details.block
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.nft.component.NFTDetailsBlockComponent
|
||||
import com.tangem.features.nft.details.block.ui.NFTDetailsBlock
|
||||
import com.tangem.features.nft.impl.R
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -16,13 +20,23 @@ class DefaultNFTDetailsBlockComponent @AssistedInject constructor(
|
|||
@Assisted private val params: NFTDetailsBlockComponent.Params,
|
||||
) : NFTDetailsBlockComponent, AppComponentContext by context {
|
||||
|
||||
private val account = params.account
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
NFTDetailsBlock(
|
||||
assetName = stringReference(params.nftAsset.name.orEmpty()),
|
||||
collectionName = stringReference(params.nftCollectionName),
|
||||
assetImage = params.nftAsset.media?.imageUrl,
|
||||
title = params.title,
|
||||
accountTitleUM = if (account != null && params.isAccountsMode) {
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.common_from),
|
||||
name = account.accountName.toUM().value,
|
||||
icon = account.icon.toUM(),
|
||||
)
|
||||
} else {
|
||||
AccountTitleUM.Text(params.walletTitle)
|
||||
},
|
||||
isSuccessScreen = params.isSuccessScreen,
|
||||
networkIconRes = getActiveIconRes(params.nftAsset.network.rawId),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.account.AccountTitle
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
|
|
@ -23,7 +25,7 @@ import com.tangem.features.nft.impl.R
|
|||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
internal fun NFTDetailsBlock(
|
||||
title: TextReference,
|
||||
accountTitleUM: AccountTitleUM,
|
||||
assetName: TextReference,
|
||||
collectionName: TextReference,
|
||||
assetImage: String?,
|
||||
|
|
@ -38,11 +40,7 @@ internal fun NFTDetailsBlock(
|
|||
.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
AccountTitle(accountTitleUM)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
|
|
@ -91,7 +89,7 @@ private fun NFTDetailsBlock_Preview() {
|
|||
collectionName = stringReference("NFT Collection"),
|
||||
assetImage = null,
|
||||
networkIconRes = R.drawable.img_polygon_22,
|
||||
title = stringReference("From My Wallet"),
|
||||
accountTitleUM = AccountTitleUM.Text(stringReference("From My Wallet")),
|
||||
isSuccessScreen = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.models.Asset
|
||||
import com.tangem.domain.models.ReceiveAddressModel
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.TokenReceiveNotification
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
|
|
@ -26,8 +23,7 @@ import com.tangem.domain.nft.GetNFTCurrencyUseCase
|
|||
import com.tangem.domain.nft.GetNFTNetworkStatusUseCase
|
||||
import com.tangem.domain.nft.GetNFTNetworksUseCase
|
||||
import com.tangem.domain.nft.analytics.NFTAnalyticsEvent
|
||||
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetEnsNameUseCase
|
||||
import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory
|
||||
import com.tangem.features.nft.impl.R
|
||||
import com.tangem.features.nft.receive.NFTReceiveComponent
|
||||
import com.tangem.features.nft.receive.entity.NFTReceiveUM
|
||||
|
|
@ -36,7 +32,6 @@ import com.tangem.features.nft.receive.entity.transformer.ToggleSearchBarTransfo
|
|||
import com.tangem.features.nft.receive.entity.transformer.UpdateDataStateTransformer
|
||||
import com.tangem.features.nft.receive.entity.transformer.UpdateSearchQueryTransformer
|
||||
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -56,9 +51,8 @@ internal class NFTReceiveModel @Inject constructor(
|
|||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
|
||||
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
|
||||
private val getEnsNameUseCase: GetEnsNameUseCase,
|
||||
private val getNFTCurrencyUseCase: GetNFTCurrencyUseCase,
|
||||
private val receiveAddressesFactory: ReceiveAddressesFactory,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -196,54 +190,11 @@ internal class NFTReceiveModel @Inject constructor(
|
|||
|
||||
private suspend fun configureReceiveAddresses(addresses: NetworkAddress, network: Network): TokenReceiveConfig {
|
||||
val cryptoCurrency = getNFTCurrencyUseCase.invoke(network)
|
||||
|
||||
val ensName = getEnsNameUseCase.invoke(
|
||||
return receiveAddressesFactory.createForNft(
|
||||
userWalletId = params.userWalletId,
|
||||
addresses = addresses,
|
||||
network = network,
|
||||
address = addresses.defaultAddress.value,
|
||||
)
|
||||
|
||||
val receiveAddresses = buildList {
|
||||
ensName?.let { ens ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Ens,
|
||||
value = ens,
|
||||
),
|
||||
)
|
||||
}
|
||||
addresses.availableAddresses.map { address ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = when (address.type) {
|
||||
NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.NameService.Default
|
||||
NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.NameService.Legacy
|
||||
},
|
||||
value = address.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val notifications = buildList {
|
||||
if (BlockchainUtils.isSolana(network.rawId)) {
|
||||
add(
|
||||
TokenReceiveNotification(
|
||||
title = R.string.nft_receive_unsupported_types,
|
||||
subtitle = R.string.nft_receive_unsupported_types_description,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return TokenReceiveConfig(
|
||||
shouldShowWarning = Asset.NFT.name !in getViewedTokenReceiveWarningUseCase(),
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
userWalletId = params.userWalletId,
|
||||
showMemoDisclaimer = false,
|
||||
receiveAddress = receiveAddresses,
|
||||
tokenReceiveNotification = notifications,
|
||||
asset = Asset.NFT,
|
||||
nft = cryptoCurrency,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,11 +14,11 @@ import com.tangem.core.error.UniversalError
|
|||
import com.tangem.core.error.ext.universalError
|
||||
import com.tangem.core.ui.utils.showErrorDialog
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.visa.model.VisaCardId
|
||||
import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.domain.visa.model.VisaActivationInput
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
|
||||
|
|
@ -57,11 +57,12 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
private val activationInput = when (val status = params.scanResponse.visaCardActivationStatus) {
|
||||
is VisaCardActivationStatus.NotStartedActivation -> status.activationInput
|
||||
is VisaCardActivationStatus.ActivationStarted -> status.activationInput
|
||||
else -> error("Visa activation status is not set or incorrect for this step")
|
||||
}
|
||||
private val activationInput: VisaActivationInput = TODO("Fix visaCardActivationStatus retrieval")
|
||||
// when (val status = params.scanResponse.visaCardActivationStatus) {
|
||||
// is VisaCardActivationStatus.NotStartedActivation -> status.activationInput
|
||||
// is VisaCardActivationStatus.ActivationStarted -> status.activationInput
|
||||
// else -> error("Visa activation status is not set or incorrect for this step")
|
||||
// }
|
||||
|
||||
private val _uiState = MutableStateFlow(getInitialState())
|
||||
|
||||
|
|
|
|||
|
|
@ -12,15 +12,15 @@ import com.tangem.core.ui.utils.showErrorDialog
|
|||
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
|
||||
import com.tangem.datasource.local.visa.VisaOTPStorage
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.domain.visa.error.VisaActivationError
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.visa.model.VisaCardId
|
||||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Config
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Params
|
||||
|
|
@ -180,13 +180,15 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
|
|||
onDone.emit(Params.DoneEvent.Activated)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateProperty")
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse, authTokens: VisaAuthTokens): UserWallet =
|
||||
withContext(dispatchers.io) {
|
||||
val newActivationStatus = VisaCardActivationStatus.Activated(visaAuthTokens = authTokens)
|
||||
|
||||
requireNotNull(
|
||||
value = coldUserWalletBuilderFactory.create(
|
||||
scanResponse = scanResponse.copy(visaCardActivationStatus = newActivationStatus),
|
||||
// scanResponse = scanResponse.copy(visaCardActivationStatus = newActivationStatus),
|
||||
scanResponse = scanResponse,
|
||||
).build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,10 +10,6 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
|
||||
import com.tangem.domain.visa.model.VisaActivationRemoteState
|
||||
import com.tangem.domain.visa.model.VisaCardActivationStatus
|
||||
import com.tangem.domain.visa.model.VisaCardWalletDataToSignRequest
|
||||
import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
|
|
@ -143,74 +139,75 @@ internal class OnboardingVisaModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun initializeRoute(): OnboardingVisaRoute {
|
||||
return when (val activationStatus = params.scanResponse.visaCardActivationStatus) {
|
||||
is VisaCardActivationStatus.ActivationStarted -> {
|
||||
when (val remoteState = activationStatus.remoteState) {
|
||||
is VisaActivationRemoteState.CardWalletSignatureRequired -> {
|
||||
if (activationStatus.activationInput.isAccessCodeSet) {
|
||||
OnboardingVisaRoute.WelcomeBack(
|
||||
activationInput = activationStatus.activationInput,
|
||||
dataToSignByCardWalletRequest = VisaCardWalletDataToSignRequest(
|
||||
activationOrderInfo = remoteState.activationOrderInfo,
|
||||
cardWalletAddress = activationStatus.cardWalletAddress,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
OnboardingVisaRoute.AccessCode
|
||||
}
|
||||
}
|
||||
is VisaActivationRemoteState.CustomerWalletSignatureRequired -> {
|
||||
remoteState.getRoute(activationStatus)
|
||||
}
|
||||
VisaActivationRemoteState.PaymentAccountDeploying -> {
|
||||
OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.Approve)
|
||||
}
|
||||
VisaActivationRemoteState.WaitingForActivationFinishing -> {
|
||||
OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.PinCode)
|
||||
}
|
||||
is VisaActivationRemoteState.AwaitingPinCode -> {
|
||||
OnboardingVisaRoute.PinCode(
|
||||
activationOrderInfo = remoteState.activationOrderInfo,
|
||||
pinCodeValidationError = false,
|
||||
)
|
||||
}
|
||||
VisaActivationRemoteState.Activated,
|
||||
VisaActivationRemoteState.BlockedForActivation,
|
||||
VisaActivationRemoteState.Failed,
|
||||
-> error("Activation status is not correct for onboarding flow")
|
||||
}
|
||||
}
|
||||
is VisaCardActivationStatus.NotStartedActivation -> OnboardingVisaRoute.Welcome
|
||||
else -> error("Visa activation status is not correct for onboarding flow")
|
||||
}
|
||||
TODO("Fix visaCardActivationStatus retrieval")
|
||||
// return when (val activationStatus = params.scanResponse.visaCardActivationStatus) {
|
||||
// is VisaCardActivationStatus.ActivationStarted -> {
|
||||
// when (val remoteState = activationStatus.remoteState) {
|
||||
// is VisaActivationRemoteState.CardWalletSignatureRequired -> {
|
||||
// if (activationStatus.activationInput.isAccessCodeSet) {
|
||||
// OnboardingVisaRoute.WelcomeBack(
|
||||
// activationInput = activationStatus.activationInput,
|
||||
// dataToSignByCardWalletRequest = VisaCardWalletDataToSignRequest(
|
||||
// activationOrderInfo = remoteState.activationOrderInfo,
|
||||
// cardWalletAddress = activationStatus.cardWalletAddress,
|
||||
// ),
|
||||
// )
|
||||
// } else {
|
||||
// OnboardingVisaRoute.AccessCode
|
||||
// }
|
||||
// }
|
||||
// is VisaActivationRemoteState.CustomerWalletSignatureRequired -> {
|
||||
// remoteState.getRoute(activationStatus)
|
||||
// }
|
||||
// VisaActivationRemoteState.PaymentAccountDeploying -> {
|
||||
// OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.Approve)
|
||||
// }
|
||||
// VisaActivationRemoteState.WaitingForActivationFinishing -> {
|
||||
// OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.PinCode)
|
||||
// }
|
||||
// is VisaActivationRemoteState.AwaitingPinCode -> {
|
||||
// OnboardingVisaRoute.PinCode(
|
||||
// activationOrderInfo = remoteState.activationOrderInfo,
|
||||
// pinCodeValidationError = false,
|
||||
// )
|
||||
// }
|
||||
// VisaActivationRemoteState.Activated,
|
||||
// VisaActivationRemoteState.BlockedForActivation,
|
||||
// VisaActivationRemoteState.Failed,
|
||||
// -> error("Activation status is not correct for onboarding flow")
|
||||
// }
|
||||
// }
|
||||
// is VisaCardActivationStatus.NotStartedActivation -> OnboardingVisaRoute.Welcome
|
||||
// else -> error("Visa activation status is not correct for onboarding flow")
|
||||
// }
|
||||
}
|
||||
|
||||
private fun VisaActivationRemoteState.CustomerWalletSignatureRequired.getRoute(
|
||||
activationStatus: VisaCardActivationStatus.ActivationStarted,
|
||||
): OnboardingVisaRoute {
|
||||
val foundWalletCardId = tryToFindExistingWalletCardId(this.activationOrderInfo.customerWalletAddress)
|
||||
val request = VisaCustomerWalletDataToSignRequest(
|
||||
orderId = this.activationOrderInfo.orderId,
|
||||
cardWalletAddress = activationStatus.cardWalletAddress,
|
||||
customerWalletAddress = this.activationOrderInfo.customerWalletAddress,
|
||||
)
|
||||
val preparationDataForApprove = PreparationDataForApprove(
|
||||
customerWalletAddress = this.activationOrderInfo.customerWalletAddress,
|
||||
request = request,
|
||||
)
|
||||
|
||||
return if (foundWalletCardId != null) {
|
||||
OnboardingVisaRoute.TangemWalletApproveOption(
|
||||
preparationDataForApprove = preparationDataForApprove,
|
||||
foundWalletCardId = foundWalletCardId,
|
||||
allowNavigateBack = false,
|
||||
)
|
||||
} else {
|
||||
OnboardingVisaRoute.ChooseWallet(
|
||||
preparationDataForApprove = preparationDataForApprove,
|
||||
)
|
||||
}
|
||||
}
|
||||
// private fun VisaActivationRemoteState.CustomerWalletSignatureRequired.getRoute(
|
||||
// activationStatus: VisaCardActivationStatus.ActivationStarted,
|
||||
// ): OnboardingVisaRoute {
|
||||
// val foundWalletCardId = tryToFindExistingWalletCardId(this.activationOrderInfo.customerWalletAddress)
|
||||
// val request = VisaCustomerWalletDataToSignRequest(
|
||||
// orderId = this.activationOrderInfo.orderId,
|
||||
// cardWalletAddress = activationStatus.cardWalletAddress,
|
||||
// customerWalletAddress = this.activationOrderInfo.customerWalletAddress,
|
||||
// )
|
||||
// val preparationDataForApprove = PreparationDataForApprove(
|
||||
// customerWalletAddress = this.activationOrderInfo.customerWalletAddress,
|
||||
// request = request,
|
||||
// )
|
||||
//
|
||||
// return if (foundWalletCardId != null) {
|
||||
// OnboardingVisaRoute.TangemWalletApproveOption(
|
||||
// preparationDataForApprove = preparationDataForApprove,
|
||||
// foundWalletCardId = foundWalletCardId,
|
||||
// allowNavigateBack = false,
|
||||
// )
|
||||
// } else {
|
||||
// OnboardingVisaRoute.ChooseWallet(
|
||||
// preparationDataForApprove = preparationDataForApprove,
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
private fun tryToFindExistingWalletCardId(targetAddress: String): String? {
|
||||
val wallets = getWalletsUseCase.invokeSync().filter { it.isLocked.not() }
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ dependencies {
|
|||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.account.status)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.onramp.selecttoken
|
||||
|
||||
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
|
||||
|
|
@ -41,11 +42,13 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state = model.state.collectAsStateWithLifecycle()
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val onrampTokenListState by onrampTokenListComponent.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
OnrampSelectToken(
|
||||
state = state.value,
|
||||
state = state,
|
||||
onrampTokenListComponent = onrampTokenListComponent,
|
||||
onrampTokenListState = onrampTokenListState,
|
||||
hotCryptoComponent = hotCryptoComponent,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,12 +21,14 @@ import com.tangem.features.onramp.hottokens.HotCryptoComponent
|
|||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.selecttoken.entity.OnrampOperationUM
|
||||
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun OnrampSelectToken(
|
||||
state: OnrampOperationUM,
|
||||
onrampTokenListComponent: OnrampTokenListComponent,
|
||||
onrampTokenListState: TokenListUM,
|
||||
hotCryptoComponent: HotCryptoComponent?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -50,8 +52,9 @@ internal fun OnrampSelectToken(
|
|||
)
|
||||
}
|
||||
|
||||
item(key = "token_list", contentType = "token_list") {
|
||||
onrampTokenListComponent.Content(
|
||||
with(onrampTokenListComponent) {
|
||||
content(
|
||||
uiState = onrampTokenListState,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.onramp.swap
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
|
|
@ -54,12 +55,16 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state = model.state.collectAsStateWithLifecycle()
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val fromTokensState by selectFromTokenListComponent.uiState.collectAsStateWithLifecycle()
|
||||
val toTokensState by selectToTokenListComponent.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
SwapSelectTokens(
|
||||
state = state.value,
|
||||
state = state,
|
||||
selectFromTokenListComponent = selectFromTokenListComponent,
|
||||
selectFromTokenListState = fromTokensState,
|
||||
selectToTokenListComponent = selectToTokenListComponent,
|
||||
selectToTokenListState = toTokensState,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,15 @@ package com.tangem.features.onramp.swap.availablepairs
|
|||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.decompose.ComposableListContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** Token list component that present list of available tokens for swap */
|
||||
@Stable
|
||||
internal interface AvailableSwapPairsComponent : ComposableContentComponent {
|
||||
internal interface AvailableSwapPairsComponent : ComposableListContentComponent<TokenListUM> {
|
||||
|
||||
/** Component factory */
|
||||
interface Factory : ComponentFactory<Params, AvailableSwapPairsComponent>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.runtime.Stable
|
||||
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.onramp.swap.availablepairs.model.AvailableSwapPairsModel
|
||||
import com.tangem.features.onramp.tokenlist.ui.TokenList
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.ui.onrampTokenList
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Stable
|
||||
internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor(
|
||||
|
|
@ -21,11 +21,11 @@ internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor(
|
|||
|
||||
private val model: AvailableSwapPairsModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
override val uiState: StateFlow<TokenListUM>
|
||||
get() = model.state
|
||||
|
||||
TokenList(state = state, modifier = modifier)
|
||||
override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) {
|
||||
onrampTokenList(state = uiState)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.entity.converters
|
||||
|
||||
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class LoadingAccountTokenItemConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
) : Converter<AccountStatus.CryptoPortfolio, TokensListItemUM.Portfolio> {
|
||||
|
||||
override fun convert(value: AccountStatus.CryptoPortfolio): TokensListItemUM.Portfolio {
|
||||
val (account, currencies) = value
|
||||
|
||||
return TokensListItemUM.Portfolio(
|
||||
tokenItemUM = AccountCryptoPortfolioItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
account = account,
|
||||
onItemClick = null,
|
||||
).convert(TotalFiatBalance.Failed),
|
||||
isExpanded = true,
|
||||
isCollapsable = false,
|
||||
tokens = currencies.flattenCurrencies().map(LoadingTokenListItemConverter::convert).toPersistentList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.entity.transformers
|
||||
|
||||
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SetNoAvailablePairsTransformerV2(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val accountList: Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>>,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val isAccountsMode: Boolean,
|
||||
private val unavailableErrorText: TextReference,
|
||||
) : TokenListUMTransformer {
|
||||
private val unavailableConverter = OnrampTokenItemStateConverterFactory
|
||||
.createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText)
|
||||
|
||||
override fun transform(prevState: TokenListUM): TokenListUM {
|
||||
return prevState.copy(
|
||||
availableItems = persistentListOf(),
|
||||
unavailableItems = persistentListOf(),
|
||||
tokensListData = if (isAccountsMode) {
|
||||
TokenListUMData.AccountList(
|
||||
tokensList = accountList.map { (account, cryptoCurrencies) ->
|
||||
TokensListItemUM.Portfolio(
|
||||
tokenItemUM = AccountCryptoPortfolioItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
account = account,
|
||||
onItemClick = null,
|
||||
).convert(TotalFiatBalance.Failed),
|
||||
isExpanded = true,
|
||||
isCollapsable = false,
|
||||
tokens = unavailableConverter.convertList(cryptoCurrencies)
|
||||
.map(TokensListItemUM::Token)
|
||||
.toPersistentList(),
|
||||
)
|
||||
}.toPersistentList(),
|
||||
)
|
||||
} else {
|
||||
TokenListUMData.TokenList(
|
||||
tokensList = accountList.flatMap { (_, cryptoCurrencies) ->
|
||||
unavailableConverter.convertList(cryptoCurrencies)
|
||||
.map(TokensListItemUM::Token)
|
||||
}.toPersistentList(),
|
||||
)
|
||||
},
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
warning = NotificationUM.Warning.SwapNoAvailablePair,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
package com.tangem.features.onramp.swap.availablepairs.model
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.fields.InputManager
|
||||
import com.tangem.core.ui.extensions.capitalize
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
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
|
||||
|
|
@ -15,6 +18,8 @@ import com.tangem.domain.core.utils.getOrElse
|
|||
import com.tangem.domain.core.utils.lceContent
|
||||
import com.tangem.domain.core.utils.lceError
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
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.tokenlist.TokenList
|
||||
|
|
@ -28,11 +33,13 @@ import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponen
|
|||
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoadingTokenItemsTransformer
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformerV2
|
||||
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
|
||||
import com.tangem.features.onramp.swap.entity.AccountCurrencyUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.transformer.*
|
||||
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
|
||||
import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer
|
||||
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
|
||||
|
|
@ -43,7 +50,7 @@ import javax.inject.Inject
|
|||
|
||||
private typealias AvailablePairsState = Lce<Throwable, List<SwapPairLeast>>
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class AvailableSwapPairsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -53,7 +60,10 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val getAvailablePairsUseCase: GetAvailablePairsUseCase,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
getWalletsUseCase: GetWalletsUseCase,
|
||||
) : Model() {
|
||||
|
||||
val state: StateFlow<TokenListUM> = tokenListUMController.state
|
||||
|
|
@ -62,13 +72,17 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
private val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
|
||||
|
||||
private val tokenListFlow = getTokenListUseCaseFlow()
|
||||
|
||||
private val accountListFlow = getAccountListUseCaseFlow()
|
||||
private val availablePairsByNetworkFlow = MutableStateFlow<Map<LeastTokenInfo, AvailablePairsState>>(emptyMap())
|
||||
|
||||
init {
|
||||
initializeSearchBarCallbacks()
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
subscribeOnUpdateStateV2()
|
||||
} else {
|
||||
subscribeOnUpdateState()
|
||||
}
|
||||
|
||||
subscribeOnUpdateState()
|
||||
initializeSearchBarCallbacks()
|
||||
subscribeOnAvailablePairsUpdates()
|
||||
}
|
||||
|
||||
|
|
@ -79,12 +93,20 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
maybeTokenList.getOrElse(
|
||||
ifLoading = { it ?: TokenList.Empty },
|
||||
ifError = { TokenList.Empty },
|
||||
)
|
||||
.flattenCurrencies()
|
||||
).flattenCurrencies()
|
||||
}
|
||||
.shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1)
|
||||
}
|
||||
|
||||
private fun getAccountListUseCaseFlow(): SharedFlow<List<AccountStatus>> {
|
||||
return singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(params.userWalletId))
|
||||
.distinctUntilChanged()
|
||||
.map { accountStatusList ->
|
||||
accountStatusList.accountStatuses.toList()
|
||||
}.flowOn(dispatchers.default)
|
||||
.shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1)
|
||||
}
|
||||
|
||||
private fun initializeSearchBarCallbacks() {
|
||||
tokenListUMController.update(
|
||||
transformer = UpdateSearchBarCallbacksTransformer(
|
||||
|
|
@ -130,6 +152,53 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnUpdateStateV2() {
|
||||
combine(
|
||||
flow = getAccountsAndModeFlow(),
|
||||
flow2 = getAppCurrencyAndBalanceHidingFlow(),
|
||||
flow3 = params.selectedStatus,
|
||||
flow4 = searchManager.query,
|
||||
flow5 = availablePairsByNetworkFlow
|
||||
.map { it[params.selectedStatus.value?.toLeastTokenInfo()] }
|
||||
.distinctUntilChanged(),
|
||||
) { accountListAndMode, appCurrencyAndBalanceHiding, selectedStatus, query, availablePairsState ->
|
||||
val (accountList, isAccountsMode) = accountListAndMode
|
||||
availablePairsState?.fold(
|
||||
ifLoading = {
|
||||
SetLoadingAccountTokenListTransformer(
|
||||
appCurrency = appCurrencyAndBalanceHiding.first,
|
||||
accountList = accountList,
|
||||
isAccountsMode = isAccountsMode,
|
||||
)
|
||||
},
|
||||
ifContent = { pairs ->
|
||||
handleContentStateV2(
|
||||
appCurrencyAndBalanceHiding = appCurrencyAndBalanceHiding,
|
||||
accountList = accountList,
|
||||
selectedStatus = selectedStatus,
|
||||
query = query,
|
||||
availablePairs = pairs,
|
||||
isAccountsMode = isAccountsMode,
|
||||
)
|
||||
},
|
||||
ifError = {
|
||||
handleErrorStateV2(
|
||||
cause = it,
|
||||
networkInfo = params.selectedStatus.value?.toLeastTokenInfo(),
|
||||
accountList = accountList,
|
||||
)
|
||||
},
|
||||
) ?: SetLoadingAccountTokenListTransformer(
|
||||
appCurrency = appCurrencyAndBalanceHiding.first,
|
||||
accountList = accountList,
|
||||
isAccountsMode = isAccountsMode,
|
||||
)
|
||||
}
|
||||
.onEach(tokenListUMController::update)
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun handleContentState(
|
||||
appCurrencyAndBalanceHiding: Pair<AppCurrency, Boolean>,
|
||||
currencies: List<CryptoCurrencyStatus>,
|
||||
|
|
@ -176,6 +245,53 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleContentStateV2(
|
||||
appCurrencyAndBalanceHiding: Pair<AppCurrency, Boolean>,
|
||||
accountList: List<AccountStatus>,
|
||||
selectedStatus: CryptoCurrencyStatus?,
|
||||
query: String,
|
||||
availablePairs: List<SwapPairLeast>,
|
||||
isAccountsMode: Boolean,
|
||||
): TokenListUMTransformer {
|
||||
val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding
|
||||
|
||||
val filterByQueryAccountList = accountList.associate { accountStatus ->
|
||||
when (accountStatus) {
|
||||
is AccountStatus.CryptoPortfolio -> accountStatus.account to accountStatus.tokenList.flattenCurrencies()
|
||||
.filter { it.currency != selectedStatus?.currency }
|
||||
.filterByQuery(query = query)
|
||||
}
|
||||
}
|
||||
|
||||
if (availablePairs.isEmpty()) {
|
||||
return SetNoAvailablePairsTransformerV2(
|
||||
appCurrency = appCurrency,
|
||||
accountList = filterByQueryAccountList,
|
||||
unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
isAccountsMode = isAccountsMode,
|
||||
)
|
||||
}
|
||||
|
||||
return if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) {
|
||||
SetNothingToFoundStateTransformerV2(
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
emptySearchMessageReference = resourceReference(
|
||||
id = R.string.action_buttons_swap_empty_search_message,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
UpdateAccountTokenListTransformer(
|
||||
appCurrency = appCurrency,
|
||||
onItemClick = params.onTokenClick,
|
||||
accountList = filterByQueryAccountList.filterByAvailability(availablePairs = availablePairs),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header),
|
||||
isAccountsMode = isAccountsMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleErrorState(
|
||||
cause: Throwable,
|
||||
networkInfo: LeastTokenInfo?,
|
||||
|
|
@ -193,6 +309,26 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun handleErrorStateV2(
|
||||
cause: Throwable,
|
||||
networkInfo: LeastTokenInfo?,
|
||||
accountList: List<AccountStatus>,
|
||||
): SetErrorWarningTransformer {
|
||||
return SetErrorWarningTransformer(
|
||||
cause = cause,
|
||||
onRefresh = {
|
||||
modelScope.launch {
|
||||
if (networkInfo != null) {
|
||||
accountList.filterIsInstance<AccountStatus.CryptoPortfolio>()
|
||||
.forEach { (_, currencies) ->
|
||||
updateAvailablePairs(networkInfo, currencies.flattenCurrencies())
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun subscribeOnAvailablePairsUpdates() {
|
||||
modelScope.launch {
|
||||
params.selectedStatus
|
||||
|
|
@ -203,9 +339,19 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() == true
|
||||
if (isAlreadyLoaded) return@collectLatest
|
||||
|
||||
val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest
|
||||
|
||||
updateAvailablePairs(networkInfo = networkInfo, statuses = statuses)
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
val accountList = accountListFlow.firstOrNull() ?: return@collectLatest
|
||||
updateAvailablePairs(
|
||||
networkInfo = networkInfo,
|
||||
statuses = accountList.filterIsInstance<AccountStatus.CryptoPortfolio>()
|
||||
.flatMap { accountStatus ->
|
||||
accountStatus.flattenCurrencies()
|
||||
}.toSet().toList(),
|
||||
)
|
||||
} else {
|
||||
val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest
|
||||
updateAvailablePairs(networkInfo = networkInfo, statuses = statuses)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -247,6 +393,14 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getAccountsAndModeFlow(): Flow<Pair<List<AccountStatus>, Boolean>> {
|
||||
return combine(
|
||||
flow = accountListFlow.distinctUntilChanged(),
|
||||
flow2 = isAccountsModeEnabledUseCase().distinctUntilChanged(),
|
||||
transform = ::Pair,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSearchQueryChange(newQuery: String) {
|
||||
if (state.value.searchBarUM.query == newQuery) return
|
||||
|
||||
|
|
@ -286,6 +440,29 @@ internal class AvailableSwapPairsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>>.filterByAvailability(
|
||||
availablePairs: List<SwapPairLeast>,
|
||||
): List<AccountAvailabilityUM> {
|
||||
return map { (account, currencies) ->
|
||||
AccountAvailabilityUM(
|
||||
account = account,
|
||||
currencyList = currencies.map { status ->
|
||||
val isAvailable = availablePairs.map(SwapPairLeast::to).contains(status.toLeastTokenInfo())
|
||||
|
||||
val isAvailableToSwap = isAvailable &&
|
||||
status.value !is CryptoCurrencyStatus.MissedDerivation &&
|
||||
status.value !is CryptoCurrencyStatus.Unreachable &&
|
||||
!status.currency.isCustom
|
||||
|
||||
AccountCurrencyUM(
|
||||
cryptoCurrencyStatus = status,
|
||||
isAvailable = isAvailableToSwap,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo {
|
||||
return LeastTokenInfo(
|
||||
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.onramp.swap.entity
|
||||
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
|
||||
internal data class AccountAvailabilityUM(
|
||||
val account: Account.CryptoPortfolio,
|
||||
val currencyList: List<AccountCurrencyUM>,
|
||||
)
|
||||
|
||||
internal data class AccountCurrencyUM(
|
||||
val isAvailable: Boolean,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
)
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.features.onramp.swap.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconUM
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
|
|
@ -11,7 +13,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
internal sealed interface ExchangeCardUM {
|
||||
|
||||
/** Title reference */
|
||||
val titleReference: TextReference
|
||||
val titleUM: TitleUM
|
||||
|
||||
/** Remove button UI model */
|
||||
val removeButtonUM: RemoveButtonUM?
|
||||
|
|
@ -19,11 +21,11 @@ internal sealed interface ExchangeCardUM {
|
|||
/**
|
||||
* Empty state
|
||||
*
|
||||
* @property titleReference title reference
|
||||
* @property titleUM title reference
|
||||
* @property subtitleReference empty token subtitle reference
|
||||
*/
|
||||
data class Empty(
|
||||
override val titleReference: TextReference,
|
||||
override val titleUM: TitleUM,
|
||||
val subtitleReference: TextReference,
|
||||
) : ExchangeCardUM {
|
||||
|
||||
|
|
@ -33,15 +35,29 @@ internal sealed interface ExchangeCardUM {
|
|||
/**
|
||||
* Filled
|
||||
*
|
||||
* @property titleReference title reference
|
||||
* @property titleUM title reference
|
||||
* @property removeButtonUM remove button UI model
|
||||
* @property tokenItemState token item state
|
||||
*/
|
||||
data class Filled(
|
||||
override val titleReference: TextReference,
|
||||
override val titleUM: TitleUM,
|
||||
override val removeButtonUM: RemoveButtonUM?,
|
||||
val tokenItemState: TokenItemState,
|
||||
) : ExchangeCardUM
|
||||
|
||||
data class RemoveButtonUM(val onClick: () -> Unit)
|
||||
|
||||
@Immutable
|
||||
sealed interface TitleUM {
|
||||
|
||||
data class Text(
|
||||
val title: TextReference,
|
||||
) : TitleUM
|
||||
|
||||
data class Account(
|
||||
val prefixText: TextReference,
|
||||
val name: TextReference,
|
||||
val icon: CryptoPortfolioIconUM,
|
||||
) : TitleUM
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.onramp.swap.entity.transformer
|
||||
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer
|
||||
|
|
@ -17,6 +18,8 @@ import com.tangem.features.onramp.swap.entity.utils.toFilled
|
|||
internal class SelectFromTokenTransformer(
|
||||
private val selectedTokenItemState: TokenItemState,
|
||||
private val onRemoveClick: () -> Unit,
|
||||
private val account: Account.CryptoPortfolio?,
|
||||
private val isAccountsMode: Boolean,
|
||||
) : SwapSelectTokensUMTransformer {
|
||||
|
||||
override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM {
|
||||
|
|
@ -24,6 +27,9 @@ internal class SelectFromTokenTransformer(
|
|||
exchangeFrom = prevState.exchangeFrom.toFilled(
|
||||
selectedTokenItemState = selectedTokenItemState,
|
||||
removeButtonUM = ExchangeCardUM.RemoveButtonUM(onClick = onRemoveClick),
|
||||
account = account,
|
||||
isAccountsMode = isAccountsMode,
|
||||
isFromCurrency = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.onramp.swap.entity.transformer
|
||||
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer
|
||||
|
|
@ -15,12 +16,19 @@ import com.tangem.features.onramp.swap.entity.utils.toFilled
|
|||
*/
|
||||
internal class SelectToTokenTransformer(
|
||||
private val selectedTokenItemState: TokenItemState,
|
||||
private val isAccountsMode: Boolean,
|
||||
private val account: Account.CryptoPortfolio?,
|
||||
) : SwapSelectTokensUMTransformer {
|
||||
|
||||
override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM {
|
||||
return prevState.copy(
|
||||
exchangeFrom = prevState.exchangeFrom.hideRemoveButton(),
|
||||
exchangeTo = prevState.exchangeTo.toFilled(selectedTokenItemState = selectedTokenItemState),
|
||||
exchangeTo = prevState.exchangeTo.toFilled(
|
||||
selectedTokenItemState = selectedTokenItemState,
|
||||
isAccountsMode = isAccountsMode,
|
||||
account = account,
|
||||
isFromCurrency = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
package com.tangem.features.onramp.swap.entity.utils
|
||||
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
|
||||
|
||||
/** Create empty exchange "from" card */
|
||||
internal fun createEmptyExchangeFrom(): ExchangeCardUM.Empty {
|
||||
return ExchangeCardUM.Empty(
|
||||
titleReference = resourceReference(id = R.string.swapping_from_title),
|
||||
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)),
|
||||
subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_swap),
|
||||
)
|
||||
}
|
||||
|
|
@ -16,7 +18,7 @@ internal fun createEmptyExchangeFrom(): ExchangeCardUM.Empty {
|
|||
/** Create empty exchange "to" card */
|
||||
internal fun createEmptyExchangeTo(): ExchangeCardUM.Empty {
|
||||
return ExchangeCardUM.Empty(
|
||||
titleReference = resourceReference(id = R.string.swapping_to_title),
|
||||
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_to_title)),
|
||||
subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_receive),
|
||||
)
|
||||
}
|
||||
|
|
@ -29,10 +31,25 @@ internal fun createEmptyExchangeTo(): ExchangeCardUM.Empty {
|
|||
*/
|
||||
internal fun ExchangeCardUM.toFilled(
|
||||
selectedTokenItemState: TokenItemState,
|
||||
account: Account.CryptoPortfolio?,
|
||||
isAccountsMode: Boolean,
|
||||
isFromCurrency: Boolean,
|
||||
removeButtonUM: ExchangeCardUM.RemoveButtonUM? = null,
|
||||
): ExchangeCardUM.Filled {
|
||||
return ExchangeCardUM.Filled(
|
||||
titleReference = titleReference,
|
||||
titleUM = if (account != null && isAccountsMode) {
|
||||
ExchangeCardUM.TitleUM.Account(
|
||||
prefixText = if (isFromCurrency) {
|
||||
resourceReference(R.string.common_from)
|
||||
} else {
|
||||
resourceReference(R.string.common_to)
|
||||
},
|
||||
name = account.accountName.toUM().value,
|
||||
icon = account.icon.toUM(),
|
||||
)
|
||||
} else {
|
||||
titleUM
|
||||
},
|
||||
tokenItemState = selectedTokenItemState,
|
||||
removeButtonUM = removeButtonUM,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ 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.ui.components.token.state.TokenItemState
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.onramp.component.SwapSelectTokensComponent
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensController
|
||||
|
|
@ -32,6 +35,8 @@ internal class SwapSelectTokensModel @Inject constructor(
|
|||
private val router: Router,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
) : Model() {
|
||||
|
||||
val state: StateFlow<SwapSelectTokensUM> = controller.state
|
||||
|
|
@ -43,9 +48,13 @@ internal class SwapSelectTokensModel @Inject constructor(
|
|||
|
||||
private val params = paramsContainer.require<SwapSelectTokensComponent.Params>()
|
||||
|
||||
private var isAccountsMode: Boolean = false
|
||||
private var account: Account.CryptoPortfolio? = null
|
||||
|
||||
init {
|
||||
controller.update { it.copy(onBackClick = ::onBackClick) }
|
||||
|
||||
subscribeOnAccountsMode()
|
||||
subscribeOnBalanceHidingSettings()
|
||||
}
|
||||
|
||||
|
|
@ -62,12 +71,19 @@ internal class SwapSelectTokensModel @Inject constructor(
|
|||
|
||||
_fromCurrencyStatus.value = status
|
||||
|
||||
controller.update(
|
||||
transformer = SelectFromTokenTransformer(
|
||||
selectedTokenItemState = selectedTokenItemState,
|
||||
onRemoveClick = ::onRemoveFromTokenClick,
|
||||
),
|
||||
)
|
||||
modelScope.launch {
|
||||
controller.update(
|
||||
transformer = SelectFromTokenTransformer(
|
||||
selectedTokenItemState = selectedTokenItemState,
|
||||
onRemoveClick = ::onRemoveFromTokenClick,
|
||||
isAccountsMode = isAccountsMode,
|
||||
account = getAccountCurrencyStatusUseCase.invokeSync(
|
||||
userWalletId = params.userWalletId,
|
||||
currency = status.currency,
|
||||
).getOrNull()?.account,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -84,7 +100,16 @@ internal class SwapSelectTokensModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
_toCurrencyStatus.value = status
|
||||
|
||||
controller.update(transformer = SelectToTokenTransformer(selectedTokenItemState))
|
||||
controller.update(
|
||||
transformer = SelectToTokenTransformer(
|
||||
selectedTokenItemState = selectedTokenItemState,
|
||||
isAccountsMode = isAccountsMode,
|
||||
account = getAccountCurrencyStatusUseCase.invokeSync(
|
||||
userWalletId = params.userWalletId,
|
||||
currency = status.currency,
|
||||
).getOrNull()?.account,
|
||||
),
|
||||
)
|
||||
|
||||
// require some delay to show state with selected "from" and "to" tokens
|
||||
delay(timeMillis = 500)
|
||||
|
|
@ -119,6 +144,16 @@ internal class SwapSelectTokensModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnAccountsMode() {
|
||||
isAccountsModeEnabledUseCase()
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
isAccountsMode = it
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onBackClick() {
|
||||
analyticsEventHandler.send(
|
||||
event = MainScreenAnalyticsEvent.ButtonClose(source = AnalyticsParam.ScreensSources.Swap),
|
||||
|
|
|
|||
|
|
@ -14,11 +14,14 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.account.AccountLabel
|
||||
import com.tangem.core.ui.components.account.AccountIconSize
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.components.rows.NetworkTitle
|
||||
|
|
@ -46,13 +49,14 @@ internal fun ExchangeCard(state: ExchangeCardUM, isBalanceHidden: Boolean, modif
|
|||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 116.dp)
|
||||
.background(
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.primary),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Title(titleReference = state.titleReference, removeButtonUM = state.removeButtonUM)
|
||||
Title(
|
||||
titleUM = state.titleUM,
|
||||
removeButtonUM = state.removeButtonUM,
|
||||
)
|
||||
|
||||
AnimatedContent(
|
||||
targetState = state,
|
||||
|
|
@ -73,16 +77,39 @@ internal fun ExchangeCard(state: ExchangeCardUM, isBalanceHidden: Boolean, modif
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(titleReference: TextReference, removeButtonUM: ExchangeCardUM.RemoveButtonUM?) {
|
||||
private fun Title(titleUM: ExchangeCardUM.TitleUM, removeButtonUM: ExchangeCardUM.RemoveButtonUM?) {
|
||||
NetworkTitle(
|
||||
title = {
|
||||
Text(
|
||||
text = titleReference.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
AnimatedContent(
|
||||
titleUM,
|
||||
) { currentState ->
|
||||
when (currentState) {
|
||||
is ExchangeCardUM.TitleUM.Account -> Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = currentState.prefixText.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
AccountLabel(
|
||||
name = currentState.name,
|
||||
icon = currentState.icon,
|
||||
iconSize = AccountIconSize.ExtraSmall,
|
||||
nameStyle = TangemTheme.typography.subtitle2,
|
||||
nameColor = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
is ExchangeCardUM.TitleUM.Text -> Text(
|
||||
text = currentState.title.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
action = { RemoveButton(state = removeButtonUM) },
|
||||
)
|
||||
|
|
@ -153,7 +180,7 @@ private class ExchangeCardUMProvider : PreviewParameterProvider<ExchangeCardUM>
|
|||
|
||||
override val values: Sequence<ExchangeCardUM> = sequenceOf(
|
||||
ExchangeCardUM.Empty(
|
||||
titleReference = resourceReference(id = R.string.swapping_from_title),
|
||||
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)),
|
||||
subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_swap),
|
||||
),
|
||||
createFilled(removeButtonUM = null),
|
||||
|
|
@ -162,7 +189,7 @@ private class ExchangeCardUMProvider : PreviewParameterProvider<ExchangeCardUM>
|
|||
|
||||
private fun createFilled(removeButtonUM: ExchangeCardUM.RemoveButtonUM?): ExchangeCardUM.Filled {
|
||||
return ExchangeCardUM.Filled(
|
||||
titleReference = resourceReference(id = R.string.swapping_from_title),
|
||||
titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)),
|
||||
removeButtonUM = removeButtonUM,
|
||||
tokenItemState = TokenItemState.Content(
|
||||
id = "1",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -23,6 +24,7 @@ import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponen
|
|||
import com.tangem.features.onramp.swap.entity.ExchangeCardUM
|
||||
import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM
|
||||
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
|
||||
/**
|
||||
* Swap select tokens
|
||||
|
|
@ -39,7 +41,9 @@ import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
|
|||
internal fun SwapSelectTokens(
|
||||
state: SwapSelectTokensUM,
|
||||
selectFromTokenListComponent: OnrampTokenListComponent,
|
||||
selectFromTokenListState: TokenListUM,
|
||||
selectToTokenListComponent: AvailableSwapPairsComponent,
|
||||
selectToTokenListState: TokenListUM,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BackHandler(onBack = state.onBackClick)
|
||||
|
|
@ -77,33 +81,33 @@ internal fun SwapSelectTokens(
|
|||
}
|
||||
|
||||
if (state.exchangeFrom is ExchangeCardUM.Empty) {
|
||||
item(key = "select_from", contentType = "select_from") {
|
||||
selectFromTokenListComponent.Content(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
with(selectFromTokenListComponent) {
|
||||
content(
|
||||
uiState = selectFromTokenListState,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.exchangeFrom is ExchangeCardUM.Filled) {
|
||||
item(key = "exchange_to", contentType = "exchange_to") {
|
||||
ExchangeCard(
|
||||
state = state.exchangeTo,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 12.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
if (selectToTokenListState.warning != NotificationUM.Warning.SwapNoAvailablePair) {
|
||||
ExchangeCard(
|
||||
state = state.exchangeTo,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 12.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.exchangeTo is ExchangeCardUM.Empty) {
|
||||
item(key = "select_to", contentType = "select_to") {
|
||||
selectToTokenListComponent.Content(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
with(selectToTokenListComponent) {
|
||||
content(
|
||||
uiState = selectToTokenListState,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
package com.tangem.features.onramp.tokenlist
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.runtime.Stable
|
||||
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.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.model.OnrampTokenListModel
|
||||
import com.tangem.features.onramp.tokenlist.ui.TokenList
|
||||
import com.tangem.features.onramp.tokenlist.ui.onrampTokenList
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Stable
|
||||
internal class DefaultOnrampTokenListComponent @AssistedInject constructor(
|
||||
|
|
@ -21,11 +21,11 @@ internal class DefaultOnrampTokenListComponent @AssistedInject constructor(
|
|||
|
||||
private val model: OnrampTokenListModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
override val uiState: StateFlow<TokenListUM>
|
||||
get() = model.state
|
||||
|
||||
TokenList(state = state, modifier = modifier)
|
||||
override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) {
|
||||
onrampTokenList(state = uiState)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -3,14 +3,15 @@ package com.tangem.features.onramp.tokenlist
|
|||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.decompose.ComposableListContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.onramp.tokenlist.entity.OnrampOperation
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
|
||||
/** Token list component that present list of token for multi-currency wallet */
|
||||
@Stable
|
||||
internal interface OnrampTokenListComponent : ComposableContentComponent {
|
||||
internal interface OnrampTokenListComponent : ComposableListContentComponent<TokenListUM> {
|
||||
|
||||
/** Component factory */
|
||||
interface Factory : ComponentFactory<Params, OnrampTokenListComponent>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,19 @@ internal data class TokenListUM(
|
|||
val searchBarUM: SearchBarUM,
|
||||
val availableItems: ImmutableList<TokensListItemUM>,
|
||||
val unavailableItems: ImmutableList<TokensListItemUM>,
|
||||
val tokensListData: TokenListUMData,
|
||||
val isBalanceHidden: Boolean,
|
||||
val warning: NotificationUM? = null,
|
||||
)
|
||||
)
|
||||
|
||||
internal sealed interface TokenListUMData {
|
||||
data class AccountList(
|
||||
val tokensList: ImmutableList<TokensListItemUM.Portfolio>,
|
||||
) : TokenListUMData
|
||||
|
||||
data class TokenList(
|
||||
val tokensList: ImmutableList<TokensListItemUM>,
|
||||
) : TokenListUMData
|
||||
|
||||
data object EmptyList : TokenListUMData
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ internal class TokenListUMController @Inject constructor() {
|
|||
),
|
||||
availableItems = persistentListOf(),
|
||||
unavailableItems = persistentListOf(),
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
isBalanceHidden = false,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.features.onramp.tokenlist.entity.transformer
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingAccountTokenItemConverter
|
||||
import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SetLoadingAccountTokenListTransformer(
|
||||
appCurrency: AppCurrency,
|
||||
private val accountList: List<AccountStatus>,
|
||||
private val isAccountsMode: Boolean,
|
||||
) : TokenListUMTransformer {
|
||||
|
||||
private val accountListItemConverter = LoadingAccountTokenItemConverter(appCurrency)
|
||||
|
||||
override fun transform(prevState: TokenListUM): TokenListUM {
|
||||
return prevState.copy(
|
||||
availableItems = persistentListOf(),
|
||||
unavailableItems = persistentListOf(),
|
||||
tokensListData = if (isAccountsMode) {
|
||||
TokenListUMData.AccountList(
|
||||
tokensList = accountListItemConverter.convertList(
|
||||
accountList.filterIsInstance<AccountStatus.CryptoPortfolio>(),
|
||||
).toPersistentList(),
|
||||
)
|
||||
} else {
|
||||
TokenListUMData.TokenList(
|
||||
tokensList = accountList.flatMap { account ->
|
||||
when (account) {
|
||||
is AccountStatus.CryptoPortfolio -> LoadingTokenListItemConverter.convertList(
|
||||
account.tokenList.flattenCurrencies(),
|
||||
)
|
||||
}
|
||||
}.toPersistentList(),
|
||||
)
|
||||
},
|
||||
warning = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -26,9 +27,9 @@ internal class SetNothingToFoundStateTransformer(
|
|||
id = emptySearchMessageReference.hashCode(),
|
||||
text = emptySearchMessageReference,
|
||||
).let(::add)
|
||||
}
|
||||
.toImmutableList(),
|
||||
}.toImmutableList(),
|
||||
unavailableItems = persistentListOf(),
|
||||
tokensListData = TokenListUMData.EmptyList,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.features.onramp.tokenlist.entity.transformer
|
||||
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class SetNothingToFoundStateTransformerV2(
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val emptySearchMessageReference: TextReference,
|
||||
) : TokenListUMTransformer {
|
||||
|
||||
override fun transform(prevState: TokenListUM): TokenListUM {
|
||||
return prevState.copy(
|
||||
availableItems = persistentListOf(),
|
||||
unavailableItems = persistentListOf(),
|
||||
tokensListData = TokenListUMData.TokenList(tokensList = buildList {
|
||||
TokensListItemUM.Text(
|
||||
id = emptySearchMessageReference.hashCode(),
|
||||
text = emptySearchMessageReference,
|
||||
).let(::add)
|
||||
}.toImmutableList()),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.features.onramp.tokenlist.entity.transformer
|
||||
|
||||
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class UpdateAccountTokenItemConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val unavailableErrorText: TextReference,
|
||||
onItemClick: (TokenItemState, CryptoCurrencyStatus) -> Unit,
|
||||
) : Converter<AccountAvailabilityUM, TokensListItemUM.Portfolio> {
|
||||
|
||||
private val availableConverter = OnrampTokenItemStateConverterFactory
|
||||
.createAvailableItemConverter(appCurrency, onItemClick)
|
||||
|
||||
private val unavailableConverter = OnrampTokenItemStateConverterFactory
|
||||
.createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText)
|
||||
|
||||
override fun convert(value: AccountAvailabilityUM): TokensListItemUM.Portfolio {
|
||||
return TokensListItemUM.Portfolio(
|
||||
tokenItemUM = AccountCryptoPortfolioItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
account = value.account,
|
||||
onItemClick = null,
|
||||
).convert(TotalFiatBalance.Failed),
|
||||
isExpanded = true,
|
||||
isCollapsable = false,
|
||||
tokens = value.currencyList.asSequence().map { (isAvailable, status) ->
|
||||
if (isAvailable) {
|
||||
availableConverter.convert(status)
|
||||
} else {
|
||||
unavailableConverter.convert(status)
|
||||
}
|
||||
}.map(TokensListItemUM::Token).toPersistentList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.features.onramp.tokenlist.entity.transformer
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class UpdateAccountTokenListTransformer(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val onItemClick: (TokenItemState, CryptoCurrencyStatus) -> Unit,
|
||||
private val accountList: List<AccountAvailabilityUM>,
|
||||
private val isBalanceHidden: Boolean,
|
||||
private val unavailableErrorText: TextReference,
|
||||
private val warning: NotificationUM? = null,
|
||||
private val isAccountsMode: Boolean,
|
||||
) : TokenListUMTransformer {
|
||||
|
||||
private val accountListItemConverter = UpdateAccountTokenItemConverter(
|
||||
appCurrency = appCurrency,
|
||||
onItemClick = onItemClick,
|
||||
unavailableErrorText = unavailableErrorText,
|
||||
)
|
||||
|
||||
private val availableConverter = OnrampTokenItemStateConverterFactory
|
||||
.createAvailableItemConverter(appCurrency, onItemClick)
|
||||
|
||||
private val unavailableConverter = OnrampTokenItemStateConverterFactory
|
||||
.createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText)
|
||||
|
||||
override fun transform(prevState: TokenListUM): TokenListUM {
|
||||
return prevState.copy(
|
||||
availableItems = persistentListOf(),
|
||||
unavailableItems = persistentListOf(),
|
||||
tokensListData = if (isAccountsMode) {
|
||||
TokenListUMData.AccountList(
|
||||
tokensList = accountListItemConverter.convertList(accountList).toPersistentList(),
|
||||
)
|
||||
} else {
|
||||
TokenListUMData.TokenList(
|
||||
tokensList = accountList.flatMap { (_, currencyList) ->
|
||||
currencyList.asSequence().map { (isAvailable, status) ->
|
||||
if (isAvailable) {
|
||||
availableConverter.convert(status)
|
||||
} else {
|
||||
unavailableConverter.convert(status)
|
||||
}
|
||||
}.map(TokensListItemUM::Token).toPersistentList()
|
||||
}.toPersistentList(),
|
||||
)
|
||||
},
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
warning = warning,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormatte
|
|||
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
|
|
@ -21,7 +22,13 @@ internal object OnrampTokenItemStateConverterFactory {
|
|||
): TokenItemStateConverter {
|
||||
return TokenItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
subtitleStateProvider = { createSubtitleState(status = it, isAvailable = true) },
|
||||
subtitleStateProvider = {
|
||||
createSubtitleState(
|
||||
status = it,
|
||||
isAvailable = true,
|
||||
text = stringReference(value = it.currency.symbol),
|
||||
)
|
||||
},
|
||||
subtitle2StateProvider = ::createSubtitle2State,
|
||||
fiatAmountStateProvider = {
|
||||
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = true)
|
||||
|
|
@ -40,7 +47,13 @@ internal object OnrampTokenItemStateConverterFactory {
|
|||
isAvailable = false,
|
||||
)
|
||||
},
|
||||
subtitleStateProvider = { createSubtitleState(status = it, isAvailable = false) },
|
||||
subtitleStateProvider = {
|
||||
createSubtitleState(
|
||||
status = it,
|
||||
text = stringReference(value = it.currency.symbol),
|
||||
isAvailable = false,
|
||||
)
|
||||
},
|
||||
subtitle2StateProvider = ::createSubtitle2State,
|
||||
fiatAmountStateProvider = {
|
||||
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = false)
|
||||
|
|
@ -48,12 +61,43 @@ internal object OnrampTokenItemStateConverterFactory {
|
|||
)
|
||||
}
|
||||
|
||||
private fun createSubtitleState(status: CryptoCurrencyStatus, isAvailable: Boolean): TokenItemState.SubtitleState {
|
||||
fun createUnavailableItemConverterV2(
|
||||
appCurrency: AppCurrency,
|
||||
unavailableErrorText: TextReference,
|
||||
): TokenItemStateConverter {
|
||||
return TokenItemStateConverter(
|
||||
appCurrency = appCurrency,
|
||||
iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) },
|
||||
titleStateProvider = {
|
||||
TokenItemState.TitleState.Content(
|
||||
text = stringReference(value = it.currency.name),
|
||||
isAvailable = false,
|
||||
)
|
||||
},
|
||||
subtitleStateProvider = {
|
||||
createSubtitleState(
|
||||
status = it,
|
||||
isAvailable = false,
|
||||
text = unavailableErrorText,
|
||||
)
|
||||
},
|
||||
subtitle2StateProvider = ::createSubtitle2State,
|
||||
fiatAmountStateProvider = {
|
||||
createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun createSubtitleState(
|
||||
status: CryptoCurrencyStatus,
|
||||
isAvailable: Boolean,
|
||||
text: TextReference,
|
||||
): TokenItemState.SubtitleState {
|
||||
return when (status.value) {
|
||||
CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading
|
||||
else -> {
|
||||
TokenItemState.SubtitleState.TextContent(
|
||||
value = stringReference(value = status.currency.symbol),
|
||||
value = text,
|
||||
isAvailable = isAvailable,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.fields.InputManager
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
|
||||
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
||||
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
|
||||
|
|
@ -13,6 +18,8 @@ import com.tangem.domain.core.lce.Lce
|
|||
import com.tangem.domain.core.utils.getOrElse
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.tokenlist.TokenList
|
||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||
|
|
@ -23,13 +30,11 @@ import com.tangem.domain.tokens.error.TokenListError
|
|||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM
|
||||
import com.tangem.features.onramp.swap.entity.AccountCurrencyUM
|
||||
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
|
||||
import com.tangem.features.onramp.tokenlist.entity.OnrampOperation
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMController
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer
|
||||
import com.tangem.features.onramp.tokenlist.entity.*
|
||||
import com.tangem.features.onramp.tokenlist.entity.transformer.*
|
||||
import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer
|
||||
import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer
|
||||
import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer
|
||||
|
|
@ -42,7 +47,9 @@ import kotlinx.coroutines.flow.*
|
|||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
typealias AccountCryptoList = Map<Account.CryptoPortfolio, List<CryptoCurrencyStatus>>
|
||||
|
||||
@Suppress("LargeClass", "LongParameterList")
|
||||
internal class OnrampTokenListModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -55,6 +62,9 @@ internal class OnrampTokenListModel @Inject constructor(
|
|||
private val rampStateManager: RampStateManager,
|
||||
private val getUserCountryUseCase: GetUserCountryUseCase,
|
||||
private val getAssetRequirementsUseCase: GetAssetRequirementsUseCase,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
) : Model() {
|
||||
|
||||
val state: StateFlow<TokenListUM> = tokenListUMController.state
|
||||
|
|
@ -71,8 +81,11 @@ internal class OnrampTokenListModel @Inject constructor(
|
|||
onActiveChange = ::onSearchBarActiveChange,
|
||||
),
|
||||
)
|
||||
|
||||
subscribeOnUpdateState()
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
subscribeOnUpdateStateV2()
|
||||
} else {
|
||||
subscribeOnUpdateState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnUpdateState() {
|
||||
|
|
@ -95,12 +108,7 @@ internal class OnrampTokenListModel @Inject constructor(
|
|||
if (query.isNotEmpty() && filterByQueryTokenList.isEmpty()) {
|
||||
SetNothingToFoundStateTransformer(
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
emptySearchMessageReference = when (params.filterOperation) {
|
||||
OnrampOperation.BUY -> R.string.action_buttons_buy_empty_search_message
|
||||
OnrampOperation.SELL -> R.string.action_buttons_sell_empty_search_message
|
||||
OnrampOperation.SWAP -> R.string.action_buttons_swap_empty_search_message
|
||||
}
|
||||
.let(::resourceReference),
|
||||
emptySearchMessageReference = getEmptySearchMessageReference(),
|
||||
)
|
||||
} else {
|
||||
val isInsufficientBalanceForSell = if (params.filterOperation == OnrampOperation.SELL) {
|
||||
|
|
@ -134,6 +142,62 @@ internal class OnrampTokenListModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnUpdateStateV2() {
|
||||
combine(
|
||||
flow = singleAccountStatusListSupplier(
|
||||
SingleAccountStatusListProducer.Params(params.userWalletId),
|
||||
).distinctUntilChanged(),
|
||||
flow2 = getAppCurrencyAndBalanceHidingFlow(),
|
||||
flow3 = isAccountsModeEnabledUseCase(),
|
||||
flow4 = searchManager.query,
|
||||
flow5 = hasRestrictionForSellFlow(),
|
||||
) { accountList, appCurrencyAndBalanceHiding, isAccountsMode, query, hasRestrictionForSell ->
|
||||
val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding
|
||||
val filterByQueryAccountList = accountList.filterAccountsByQuery(query)
|
||||
|
||||
if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) {
|
||||
updateTokenListUM(
|
||||
SetNothingToFoundStateTransformerV2(
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
emptySearchMessageReference = getEmptySearchMessageReference(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
updateTokenListUM(
|
||||
SetLoadingAccountTokenListTransformer(
|
||||
appCurrency = appCurrency,
|
||||
accountList = accountList.accountStatuses.toList(),
|
||||
isAccountsMode = isAccountsMode,
|
||||
),
|
||||
)
|
||||
updateTokenListUM(
|
||||
UpdateAccountTokenListTransformer(
|
||||
appCurrency = appCurrency,
|
||||
onItemClick = params.onTokenClick,
|
||||
accountList = filterByQueryAccountList.filterByAvailability(),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
unavailableErrorText = getUnavailableTokensHeaderReference(),
|
||||
warning = getSellWarning(
|
||||
hasRestrictionForSell = hasRestrictionForSell,
|
||||
isInsufficientBalanceForSell = accountList.isInsufficientBalanceForSell(),
|
||||
),
|
||||
isAccountsMode = isAccountsMode,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun getAppCurrencyAndBalanceHidingFlow(): Flow<Pair<AppCurrency, Boolean>> {
|
||||
return combine(
|
||||
flow = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(),
|
||||
flow2 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(),
|
||||
transform = ::Pair,
|
||||
)
|
||||
}
|
||||
|
||||
private fun hasRestrictionForSellFlow(): Flow<Boolean> {
|
||||
return if (params.filterOperation == OnrampOperation.SELL) {
|
||||
getUserCountryUseCase().map { maybe ->
|
||||
|
|
@ -154,25 +218,52 @@ internal class OnrampTokenListModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun AccountStatusList.isInsufficientBalanceForSell(): Boolean {
|
||||
return if (params.filterOperation == OnrampOperation.SELL) {
|
||||
(totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() == true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUnavailableTokensHeaderReference() = when (params.filterOperation) {
|
||||
OnrampOperation.BUY -> R.string.tokens_list_unavailable_to_purchase_header
|
||||
OnrampOperation.SELL -> R.string.tokens_list_unavailable_to_sell_header
|
||||
OnrampOperation.SWAP -> R.string.tokens_list_unavailable_to_swap_source_header
|
||||
}.let(::resourceReference)
|
||||
|
||||
private fun getEmptySearchMessageReference() = when (params.filterOperation) {
|
||||
OnrampOperation.BUY -> R.string.action_buttons_buy_empty_search_message
|
||||
OnrampOperation.SELL -> R.string.action_buttons_sell_empty_search_message
|
||||
OnrampOperation.SWAP -> R.string.action_buttons_swap_empty_search_message
|
||||
}.let(::resourceReference)
|
||||
|
||||
private fun updateTokenListUM(transformer: TokenListUMTransformer) {
|
||||
tokenListUMController.update { prevState ->
|
||||
transformer.transform(prevState).apply {
|
||||
if (isFirstInitialization(prevState = prevState, newState = this)) {
|
||||
params.onTokenListInitialized()
|
||||
modelScope.launch {
|
||||
tokenListUMController.update { prevState ->
|
||||
transformer.transform(prevState).apply {
|
||||
if (isFirstInitialization(prevState = prevState, newState = this)) {
|
||||
params.onTokenListInitialized()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSellWarning(hasRestrictionForSell: Boolean, isInsufficientBalanceForSell: Boolean) = when {
|
||||
hasRestrictionForSell -> NotificationUM.Warning.SellingRegionalRestriction
|
||||
isInsufficientBalanceForSell -> NotificationUM.Warning.InsufficientBalanceForSelling
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun isFirstInitialization(prevState: TokenListUM, newState: TokenListUM): Boolean {
|
||||
return prevState.availableItems.isEmpty() && prevState.unavailableItems.isEmpty() &&
|
||||
(newState.availableItems.isNotEmpty() || newState.unavailableItems.isNotEmpty())
|
||||
return if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
prevState.tokensListData == TokenListUMData.EmptyList &&
|
||||
newState.tokensListData != TokenListUMData.EmptyList
|
||||
} else {
|
||||
prevState.availableItems.isEmpty() && prevState.unavailableItems.isEmpty() &&
|
||||
(newState.availableItems.isNotEmpty() || newState.unavailableItems.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchQueryChange(newQuery: String) {
|
||||
|
|
@ -195,6 +286,16 @@ internal class OnrampTokenListModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun AccountStatusList.filterAccountsByQuery(query: String) = accountStatuses.asSequence()
|
||||
.associate { accountStatus ->
|
||||
when (accountStatus) {
|
||||
is AccountStatus.CryptoPortfolio -> {
|
||||
val filteredList = accountStatus.tokenList.flattenCurrencies().filterByQuery(query = query)
|
||||
accountStatus.account to filteredList
|
||||
}
|
||||
}
|
||||
}.filter { (_, value) -> value.isNotEmpty() }
|
||||
|
||||
private fun List<CryptoCurrencyStatus>.filterByQuery(query: String): List<CryptoCurrencyStatus> {
|
||||
return filter {
|
||||
it.currency.name.contains(other = query, ignoreCase = true) ||
|
||||
|
|
@ -237,6 +338,49 @@ internal class OnrampTokenListModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun AccountCryptoList.filterByAvailability(): List<AccountAvailabilityUM> {
|
||||
return coroutineScope {
|
||||
map { (account, currencies) ->
|
||||
async {
|
||||
AccountAvailabilityUM(
|
||||
account = account,
|
||||
currencyList = currencies.map { status ->
|
||||
val isOperationAvailable = checkAvailabilityByOperation(status = status)
|
||||
val isNotMissedDerivation = status.value !is CryptoCurrencyStatus.MissedDerivation
|
||||
val isNotLoading = status.value !is CryptoCurrencyStatus.Loading
|
||||
|
||||
val requirements = getAssetRequirementsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = status.currency,
|
||||
).getOrNull()
|
||||
|
||||
val isAvailableForBuy = rampStateManager.checkAssetRequirements(requirements)
|
||||
val isNotUnreachable = status.value !is CryptoCurrencyStatus.Unreachable
|
||||
|
||||
val isAvailable = when (params.filterOperation) {
|
||||
OnrampOperation.BUY -> {
|
||||
isAvailableForBuy
|
||||
} // unreachable state is available for Buy operation
|
||||
OnrampOperation.SELL -> isNotUnreachable
|
||||
OnrampOperation.SWAP -> {
|
||||
isNotUnreachable && isAvailableForBuy
|
||||
}
|
||||
}
|
||||
|
||||
val isTotalAvailable =
|
||||
isOperationAvailable && isNotMissedDerivation && isNotLoading && isAvailable
|
||||
|
||||
AccountCurrencyUM(
|
||||
cryptoCurrencyStatus = status,
|
||||
isAvailable = isTotalAvailable,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkAvailabilityByOperation(status: CryptoCurrencyStatus): Boolean {
|
||||
return when (params.filterOperation) {
|
||||
OnrampOperation.BUY -> {
|
||||
|
|
|
|||
|
|
@ -3,32 +3,34 @@ package com.tangem.features.onramp.tokenlist.ui
|
|||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
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.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.components.tokenlist.PortfolioListItem
|
||||
import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem
|
||||
import com.tangem.core.ui.components.tokenlist.TokenListItem
|
||||
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.BuyTokenScreenTestTags
|
||||
import com.tangem.core.ui.utils.lazyListItemPosition
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUM
|
||||
import com.tangem.features.onramp.tokenlist.entity.TokenListUMData
|
||||
import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
@ -36,17 +38,21 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
* Token list
|
||||
*
|
||||
* @param state state
|
||||
* @param modifier modifier
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) {
|
||||
Column(modifier) {
|
||||
if (state.warning == null) {
|
||||
SearchBar(searchBarUM = state.searchBarUM)
|
||||
} else {
|
||||
AnimatedContent(targetState = state.warning, label = "") { warning ->
|
||||
internal fun LazyListScope.onrampTokenList(state: TokenListUM) {
|
||||
val itemModifier = Modifier.padding(horizontal = 16.dp)
|
||||
|
||||
if (state.warning == null) {
|
||||
searchBarItem(searchBarUM = state.searchBarUM, modifier = itemModifier)
|
||||
} else {
|
||||
item("NotificationsKey") {
|
||||
AnimatedContent(
|
||||
targetState = state.warning,
|
||||
label = "",
|
||||
modifier = itemModifier,
|
||||
) { warning ->
|
||||
when (warning) {
|
||||
is NotificationUM.Warning.OnrampErrorNotification -> {
|
||||
Notification(
|
||||
|
|
@ -60,31 +66,45 @@ internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.availableItems.isNotEmpty()) {
|
||||
SpacerH12()
|
||||
ItemsBlock(items = state.availableItems, isBalanceHidden = state.isBalanceHidden)
|
||||
}
|
||||
tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden)
|
||||
|
||||
if (state.unavailableItems.isNotEmpty()) {
|
||||
SpacerH12()
|
||||
ItemsBlock(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden)
|
||||
tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden)
|
||||
|
||||
when (val list = state.tokensListData) {
|
||||
is TokenListUMData.AccountList -> list.tokensList.forEach { item ->
|
||||
portfolioTokensList(
|
||||
portfolio = item,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
)
|
||||
}
|
||||
is TokenListUMData.TokenList -> {
|
||||
tokensList(
|
||||
items = list.tokensList,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
)
|
||||
}
|
||||
TokenListUMData.EmptyList -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchBar(searchBarUM: SearchBarUM) {
|
||||
SearchBar(
|
||||
state = searchBarUM,
|
||||
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
|
||||
)
|
||||
private fun LazyListScope.searchBarItem(searchBarUM: SearchBarUM, modifier: Modifier = Modifier) {
|
||||
item("SearchKey") {
|
||||
SearchBar(
|
||||
state = searchBarUM,
|
||||
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ItemsBlock(items: ImmutableList<TokensListItemUM>, isBalanceHidden: Boolean) {
|
||||
items.fastForEachIndexed { index, item ->
|
||||
key(item.id) {
|
||||
private fun LazyListScope.tokensList(items: ImmutableList<TokensListItemUM>, isBalanceHidden: Boolean) {
|
||||
itemsIndexed(
|
||||
items = items,
|
||||
key = { _, item -> item.id },
|
||||
contentType = { _, item -> item::class.java },
|
||||
itemContent = { index, item ->
|
||||
TokenListItem(
|
||||
state = item,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
|
|
@ -92,13 +112,70 @@ private fun ItemsBlock(items: ImmutableList<TokensListItemUM>, isBalanceHidden:
|
|||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = items.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
backgroundColor = TangemTheme.colors.background.primary,
|
||||
)
|
||||
.testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
|
||||
.semantics { lazyListItemPosition = index },
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun LazyListScope.portfolioTokensList(portfolio: TokensListItemUM.Portfolio, isBalanceHidden: Boolean) {
|
||||
val tokens = portfolio.tokens
|
||||
val isExpanded = portfolio.isExpanded
|
||||
|
||||
portfolioItem(
|
||||
portfolio = portfolio,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
if (!isExpanded) return
|
||||
itemsIndexed(
|
||||
items = tokens,
|
||||
key = { _, item -> item.id },
|
||||
contentType = { _, item -> item::class.java },
|
||||
itemContent = { tokenIndex, token ->
|
||||
val indexWithHeader = tokenIndex.inc()
|
||||
PortfolioTokensListItem(
|
||||
state = token,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = indexWithHeader,
|
||||
lastIndex = tokens.lastIndex.inc(),
|
||||
backgroundColor = TangemTheme.colors.background.primary,
|
||||
)
|
||||
.conditional(tokenIndex == tokens.lastIndex) {
|
||||
Modifier.padding(bottom = 8.dp)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun LazyListScope.portfolioItem(
|
||||
portfolio: TokensListItemUM.Portfolio,
|
||||
modifier: Modifier,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
item(
|
||||
key = "account-${portfolio.id}",
|
||||
contentType = "account",
|
||||
) {
|
||||
PortfolioListItem(
|
||||
state = portfolio,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = 0,
|
||||
lastIndex = portfolio.tokens.lastIndex.inc(),
|
||||
backgroundColor = TangemTheme.colors.background.primary,
|
||||
)
|
||||
.then(modifier),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,12 +184,12 @@ private fun ItemsBlock(items: ImmutableList<TokensListItemUM>, isBalanceHidden:
|
|||
@Composable
|
||||
private fun Preview_TokenList(@PreviewParameter(PreviewTokenListUMProvider::class) state: TokenListUM) {
|
||||
TangemThemePreview {
|
||||
TokenList(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = TangemTheme.colors.background.secondary)
|
||||
.padding(16.dp),
|
||||
)
|
||||
LazyColumn(
|
||||
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
|
||||
) {
|
||||
onrampTokenList(
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
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