Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-30 14:23:50 +03:00
commit c32a5fa18d
711 changed files with 18558 additions and 4312 deletions

View file

@ -21,4 +21,5 @@ dependencies {
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.account)
}

View file

@ -1,24 +0,0 @@
package com.tangem.features.account
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.account.Account
import kotlinx.coroutines.flow.StateFlow
interface AccountSelectorComponent : ComposableBottomSheetComponent {
data class Params(
val onDismiss: () -> Unit,
val accountsBalanceFetcher: AccountsBalanceFetcher,
val controller: AccountSelectorController,
)
interface Factory {
fun create(appComponentContext: AppComponentContext, params: Params): AccountSelectorComponent
}
}
interface AccountSelectorController {
val selectedAccount: StateFlow<Account?>
fun selectAccount(account: Account?)
}

View file

@ -1,9 +1,9 @@
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.account.Account
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.error.TokenListError
@ -11,7 +11,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface AccountsBalanceFetcher {
interface PortfolioFetcher {
val data: Flow<Data>
@ -21,12 +21,15 @@ interface AccountsBalanceFetcher {
data class Data(
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
val balances: Map<UserWallet, Map<Account, AccountBalance>>,
val balances: Map<UserWallet, PortfolioBalance>,
)
data class AccountBalance(
val balance: Lce<TokenListError, TotalFiatBalance>,
)
data class PortfolioBalance(
val walletBalance: Lce<TokenListError, TotalFiatBalance>,
val accountsBalance: AccountStatusList,
) {
val userWallet: UserWallet get() = accountsBalance.userWallet
}
sealed interface Mode {
data class All(val onlyMultiCurrency: Boolean) : Mode
@ -34,6 +37,6 @@ interface AccountsBalanceFetcher {
}
interface Factory {
fun create(mode: Mode, scope: CoroutineScope): AccountsBalanceFetcher
fun create(mode: Mode, scope: CoroutineScope): PortfolioFetcher
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.features.account
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
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.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface PortfolioSelectorComponent : ComposableBottomSheetComponent, ComposableContentComponent {
val title: StateFlow<TextReference>
data class Params(
val onDismiss: () -> Unit,
val portfolioFetcher: PortfolioFetcher,
val controller: PortfolioSelectorController,
)
interface Factory {
fun create(appComponentContext: AppComponentContext, params: Params): PortfolioSelectorComponent
}
}
/**
* if [isAccountMode] is false it's mean [selectedAccount] emit [AccountId] for Main account
*/
interface PortfolioSelectorController {
val isAccountMode: Flow<Boolean>
val selectedAccount: Flow<AccountId?>
fun selectAccount(accountId: AccountId?)
fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, Account>?>
}

View file

@ -14,6 +14,7 @@ android {
dependencies {
/** Api */
implementation(projects.features.account.api)
implementation(projects.features.wallet.api)
/** Core modules */
implementation(projects.core.analytics)
@ -29,6 +30,7 @@ dependencies {
/** Domain */
implementation(projects.domain.models)
implementation(projects.domain.account)
implementation(projects.domain.account.status)
implementation(projects.domain.core)
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
@ -39,6 +41,9 @@ dependencies {
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
/** Tangem libraries */
implementation(tangemDeps.card.core)
/** Common */
implementation(projects.common.ui)
implementation(projects.common.routing)

View file

@ -60,7 +60,7 @@ internal class AccountCreateEditModel @Inject constructor(
private val umBuilder = AccountCreateEditUMBuilder(params)
val uiState: StateFlow<AccountCreateEditUM>
field = MutableStateFlow(value = getInitialState())
field = MutableStateFlow(value = getInitialState())
init {
if (params is AccountCreateEditComponent.Params.Create) {

View file

@ -1,17 +1,18 @@
package com.tangem.features.account.fetcher
import arrow.core.getOrElse
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.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
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.AccountsBalanceFetcher
import com.tangem.features.account.AccountsBalanceFetcher.*
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioFetcher.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -20,14 +21,17 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
internal class DefaultAccountsBalanceFetcher @AssistedInject constructor(
@Suppress("LongParameterList")
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,
private val dispatchers: CoroutineDispatcherProvider,
dispatchers: CoroutineDispatcherProvider,
@Assisted mode: Mode,
@Assisted private val scope: CoroutineScope,
) : AccountsBalanceFetcher {
) : PortfolioFetcher {
private val _mode = MutableStateFlow(mode)
private val _data = MutableSharedFlow<Data>(
@ -73,33 +77,22 @@ internal class DefaultAccountsBalanceFetcher @AssistedInject constructor(
}
}
private fun balancesForWallets(wallets: List<UserWallet>): Flow<Map<UserWallet, Map<Account, AccountBalance>>> =
wallets.asFlow()
.map { walletAccountsBalancesFlow(it) }
.mapLatest { accountsBalances -> combine(accountsBalances) { pairs -> pairs.toMap() } }
.flattenConcat()
private fun walletAccountsBalancesFlow(wallet: UserWallet): Flow<Pair<UserWallet, Map<Account, AccountBalance>>> =
walletAccounts(wallet)
.distinctUntilChanged()
.map { list -> list.map(::accountBalanceFlow) }
.mapLatest { balanceFlows -> combine(balanceFlows) { balances -> balances.toMap() } }
.flattenConcat()
.map { accountBalance -> wallet to accountBalance }
private fun walletAccounts(wallet: UserWallet): Flow<List<Account>> = flow {
// todo account load accounts
val accounts: List<Account> = Account.CryptoPortfolio
.createMainAccount(wallet.walletId)
.let(::listOf)
emit(accounts)
private fun balancesForWallets(wallets: List<UserWallet>): Flow<Map<UserWallet, PortfolioBalance>> {
val balanceFlows = wallets.map { walletAccountsBalancesFlow(it) }
return combine(balanceFlows) { pairs -> pairs.toMap() }
}
private fun accountBalanceFlow(account: Account): Flow<Pair<Account, AccountBalance>> = flow {
// todo account load balance
val balance = AccountBalance(balance = Lce.Content(TotalFiatBalance.Loading))
emit(account to balance)
}
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 accountStatusListFlow(wallet: UserWallet): Flow<AccountStatusList> =
singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(wallet.walletId))
private fun appCurrencyFlow(): Flow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
@ -114,7 +107,7 @@ internal class DefaultAccountsBalanceFetcher @AssistedInject constructor(
}
@AssistedFactory
interface Factory : AccountsBalanceFetcher.Factory {
override fun create(mode: Mode, scope: CoroutineScope): DefaultAccountsBalanceFetcher
interface Factory : PortfolioFetcher.Factory {
override fun create(mode: Mode, scope: CoroutineScope): DefaultPortfolioFetcher
}
}

View file

@ -1,87 +0,0 @@
package com.tangem.features.account.selector
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
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.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.features.account.AccountSelectorComponent
import com.tangem.features.account.AccountsBalanceFetcher
import com.tangem.features.account.selector.entity.AccountSelectorItemUM
import com.tangem.features.account.selector.entity.AccountSelectorUM
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.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@ModelScoped
internal class AccountSelectorModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<AccountSelectorComponent.Params>()
private val balanceFetcher get() = params.accountsBalanceFetcher
private val selectorController get() = params.controller
internal val state: StateFlow<AccountSelectorUM>
field = MutableStateFlow<AccountSelectorUM>(emptyState())
init {
balanceFetcher.data
.map { data ->
AccountSelectorUM(
isSingleWallet = balanceFetcher.mode.value is AccountsBalanceFetcher.Mode.Wallet,
items = buildUiList(data).toImmutableList(),
)
}
.launchIn(modelScope)
}
private fun buildUiList(data: AccountsBalanceFetcher.Data) = buildList {
fun Account.accountItemState(balance: Lce<TokenListError, TotalFiatBalance>): TokenItemState {
val totalFiatBalance = balance.getOrElse(
ifError = { TotalFiatBalance.Failed },
ifLoading = { TotalFiatBalance.Loading },
)
return when (this) {
is Account.CryptoPortfolio -> AccountCryptoPortfolioItemStateConverter(
appCurrency = data.appCurrency,
account = this,
onItemClick = { selectorController.selectAccount(it) },
).convert(totalFiatBalance)
}
}
data.balances.forEach { wallet, accounts ->
if (wallet.isLocked) return@forEach
AccountSelectorItemUM.Wallet(
id = wallet.walletId.stringValue,
name = stringReference(wallet.name),
).let(::add)
accounts.forEach { account, balance ->
AccountSelectorItemUM.Account(
account = account.accountItemState(balance.balance),
isBalanceHidden = data.isBalanceHidden,
).let(::add)
}
}
}
private fun emptyState() = AccountSelectorUM(
items = persistentListOf(),
balanceFetcher.mode.value is AccountsBalanceFetcher.Mode.Wallet,
)
}

View file

@ -1,69 +0,0 @@
package com.tangem.features.account.selector
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
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.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.account.AccountSelectorComponent
import com.tangem.features.account.impl.R
import com.tangem.features.account.selector.ui.AccountSelectorContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultAccountSelectorComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: AccountSelectorComponent.Params,
) : AppComponentContext by appComponentContext, AccountSelectorComponent {
private val model: AccountSelectorModel = getOrCreateModel(params)
override fun dismiss() {
params.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.state.collectAsStateWithLifecycle()
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
),
onBack = ::dismiss,
containerColor = TangemTheme.colors.background.primary,
title = {
TangemModalBottomSheetTitle(
title = resourceReference(R.string.common_choose_wallet),
startIconRes = R.drawable.ic_back_24,
onStartClick = ::dismiss,
)
},
content = {
AccountSelectorContent(
state = state,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
)
},
)
}
@AssistedFactory
interface Factory : AccountSelectorComponent.Factory {
override fun create(
appComponentContext: AppComponentContext,
params: AccountSelectorComponent.Params,
): DefaultAccountSelectorComponent
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.features.account.selector
import com.tangem.domain.models.account.Account
import com.tangem.features.account.AccountSelectorController
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
internal class DefaultAccountSelectorController @Inject constructor() : AccountSelectorController {
private val _selectedAccount: MutableStateFlow<Account?> = MutableStateFlow(null)
override val selectedAccount: StateFlow<Account?> get() = _selectedAccount
override fun selectAccount(account: Account?) {
_selectedAccount.update { account }
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.features.account.selector
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.extensions.TextReference
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.account.selector.ui.PortfolioSelectorBS
import com.tangem.features.account.selector.ui.PortfolioSelectorContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
internal class DefaultPortfolioSelectorComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: PortfolioSelectorComponent.Params,
) : AppComponentContext by appComponentContext, PortfolioSelectorComponent {
private val model: PortfolioSelectorModel = getOrCreateModel(params)
override val title: StateFlow<TextReference>
get() = model.state
.map { it.title }
.stateIn(componentScope, SharingStarted.Lazily, model.state.value.title)
override fun dismiss() {
params.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.state.collectAsStateWithLifecycle()
PortfolioSelectorBS(state, onDismiss = ::dismiss)
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
PortfolioSelectorContent(
state = state,
modifier = modifier,
)
}
@AssistedFactory
interface Factory : PortfolioSelectorComponent.Factory {
override fun create(
appComponentContext: AppComponentContext,
params: PortfolioSelectorComponent.Params,
): DefaultPortfolioSelectorComponent
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.features.account.selector
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.account.PortfolioSelectorController
import com.tangem.features.account.PortfolioFetcher
import kotlinx.coroutines.flow.*
import javax.inject.Inject
internal class DefaultPortfolioSelectorController @Inject constructor(
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
) : PortfolioSelectorController {
private val _selectedAccount: MutableStateFlow<AccountId?> = MutableStateFlow(null)
override val isAccountMode: Flow<Boolean> by lazy { isAccountsModeEnabledUseCase() }
override val selectedAccount: StateFlow<AccountId?> get() = _selectedAccount
override fun selectAccount(accountId: AccountId?) {
_selectedAccount.update { accountId }
}
override fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, Account>?> =
combine(
flow = _selectedAccount,
flow2 = portfolioFetcher.data,
transform = { accountId, data ->
accountId ?: return@combine null
var result: Pair<UserWallet, Account>? = null
data.balances.forEach { wallet, balance ->
val accountStatuses = balance.accountsBalance.accountStatuses
.find { accountId == it.account.accountId }
if (accountStatuses != null) result = wallet to accountStatuses.account
}
return@combine result
},
)
}

View file

@ -0,0 +1,186 @@
package com.tangem.features.account.selector
import com.tangem.common.ui.account.AccountPortfolioItemUMConverter
import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
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.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.account.impl.R
import com.tangem.features.account.selector.entity.PortfolioSelectorItemUM
import com.tangem.features.account.selector.entity.PortfolioSelectorUM
import com.tangem.features.wallet.utils.UserWalletImageFetcher
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
@ModelScoped
internal class PortfolioSelectorModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val walletImageFetcher: UserWalletImageFetcher,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
) : Model() {
private val params = paramsContainer.require<PortfolioSelectorComponent.Params>()
private val balanceFetcher get() = params.portfolioFetcher
private val selectorController get() = params.controller
internal val state: StateFlow<PortfolioSelectorUM>
field = MutableStateFlow<PortfolioSelectorUM>(emptyState())
init {
combine(
flow = isAccountsModeEnabledUseCase(),
flow2 = loadBalanceWithArtwork(),
transform = { isAccountsMode, (portfolioData, artworks) ->
val uiList = buildUiList(isAccountsMode, portfolioData, artworks)
val title = when (isAccountsMode) {
true -> resourceReference(R.string.common_choose_account)
false -> resourceReference(R.string.common_choose_wallet)
}
state.value = PortfolioSelectorUM(
title = title,
items = uiList.toImmutableList(),
)
},
)
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
private fun buildUiList(
isAccountsMode: Boolean,
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): List<PortfolioSelectorItemUM> = when (isAccountsMode) {
true -> buildAccountsList(portfolioData, artworks)
false -> buildWalletList(portfolioData, artworks)
}
private fun buildWalletList(
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): 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 walletItemUM = UserWalletItemUMConverter(
onClick = {
// todo account
// selectorController.selectAccount(portfolio.accountsBalance.mainAccount)
},
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = isBalanceHidden,
artwork = artworks[wallet.walletId],
isAuthMode = false,
).convert(wallet)
if (walletItemUM.isEnabled) {
add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
} else {
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
}
}
if (lockedWallets.isNotEmpty()) {
val lockedWalletsTitle = PortfolioSelectorItemUM.GroupTitle(
id = "lockedWalletsTitleId",
name = resourceReference(R.string.common_locked_wallets),
)
add(lockedWalletsTitle)
addAll(lockedWallets)
}
}
private fun buildAccountsList(
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): 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 walletItemUM = UserWalletItemUMConverter(
onClick = {
// todo account
// selectorController.selectAccount(portfolio.accountsBalance.mainAccount)
},
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = isBalanceHidden,
artwork = artworks[wallet.walletId],
isAuthMode = false,
).convert(wallet)
if (!walletItemUM.isEnabled) {
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
return@forEach
}
val walletTitle = PortfolioSelectorItemUM.GroupTitle(
id = "GroupTitle ${wallet.walletId.stringValue}",
name = stringReference(wallet.name),
)
add(walletTitle)
add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
portfolio.accountsBalance.accountStatuses.forEach { accountStatus ->
val account = accountStatus.account
val accountBalance = when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance
}
val accountItemUM = AccountPortfolioItemUMConverter(
onClick = { selectorController.selectAccount(account.accountId) },
appCurrency = appCurrency,
accountBalance = accountBalance,
isBalanceHidden = isBalanceHidden,
).convert(account)
add(PortfolioSelectorItemUM.Portfolio(accountItemUM))
}
}
if (lockedWallets.isNotEmpty()) {
val lockedWalletsTitle = PortfolioSelectorItemUM.GroupTitle(
id = "lockedWalletsTitleId",
name = resourceReference(R.string.common_locked_wallets),
)
add(lockedWalletsTitle)
addAll(lockedWallets)
}
}
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,
)
}

View file

@ -1,28 +0,0 @@
package com.tangem.features.account.selector.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
data class AccountSelectorUM(
val items: ImmutableList<AccountSelectorItemUM>,
val isSingleWallet: Boolean,
)
@Immutable
sealed interface AccountSelectorItemUM {
val id: String
data class Wallet(
override val id: String,
val name: TextReference,
) : AccountSelectorItemUM
data class Account(
val account: TokenItemState,
val isBalanceHidden: Boolean,
) : AccountSelectorItemUM {
override val id: String = account.id
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.account.selector.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
data class PortfolioSelectorUM(
val title: TextReference,
val items: ImmutableList<PortfolioSelectorItemUM>,
)
@Immutable
sealed interface PortfolioSelectorItemUM {
val id: String
data class GroupTitle(
override val id: String,
val name: TextReference,
) : PortfolioSelectorItemUM
data class Portfolio(
val item: UserWalletItemUM,
) : PortfolioSelectorItemUM {
override val id: String = item.id.stringValue
}
}

View file

@ -1,146 +0,0 @@
package com.tangem.features.account.selector.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.token.AccountItemPreviewData
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.account.selector.entity.AccountSelectorItemUM
import com.tangem.features.account.selector.entity.AccountSelectorUM
import com.tangem.features.account.selector.ui.AccountSelectorPreviewData.firstList
import kotlinx.collections.immutable.toImmutableList
import java.util.UUID
@Composable
internal fun AccountSelectorContent(
state: AccountSelectorUM,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(),
) {
LazyColumn(
modifier = modifier,
contentPadding = contentPadding,
) {
val items = state.items
itemsIndexed(
items = items,
key = { _, item -> item.id },
) { index, item ->
val previewItem = items.getOrNull(index.dec())
val offsetModifier = when {
previewItem == null -> Modifier
state.isSingleWallet -> Modifier.padding(
top = TangemTheme.dimens.spacing16,
)
item is AccountSelectorItemUM.Wallet -> Modifier.padding(
top = TangemTheme.dimens.spacing16,
)
else -> Modifier.padding(
top = TangemTheme.dimens.spacing8,
)
}
when (item) {
is AccountSelectorItemUM.Account -> TokenItem(
state = item.account,
isBalanceHidden = item.isBalanceHidden,
modifier = offsetModifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.background(color = TangemTheme.colors.background.primary),
)
is AccountSelectorItemUM.Wallet -> WalletNameRow(
model = item,
modifier = offsetModifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
}
}
}
@Composable
private fun WalletNameRow(model: AccountSelectorItemUM.Wallet, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = model.name.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AccountSelectorContentPreview(
@PreviewParameter(AccountSelectorPreviewStateProvider::class) params: AccountSelectorUM,
) {
TangemThemePreview {
AccountSelectorContent(
state = params,
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
)
}
}
internal object AccountSelectorPreviewData {
val firstList
get() = buildList {
AccountSelectorItemUM.Wallet(
id = UUID.randomUUID().toString(),
name = stringReference("Tangem 2.0"),
).let(::add)
AccountItemPreviewData.accountItem
.let { AccountSelectorItemUM.Account(it, false) }
.let(::add)
AccountItemPreviewData.accountItem.copy(iconState = AccountItemPreviewData.accountLetterIcon)
.let { AccountSelectorItemUM.Account(it, false) }
.let(::add)
AccountSelectorItemUM.Wallet(
id = UUID.randomUUID().toString(),
name = stringReference("Tangem White"),
).let(::add)
AccountItemPreviewData.accountItem.copy(iconState = AccountItemPreviewData.accountLetterIcon)
.let { AccountSelectorItemUM.Account(it, false) }
.let(::add)
}
}
internal class AccountSelectorPreviewStateProvider : CollectionPreviewParameterProvider<AccountSelectorUM>(
buildList {
val secondList = listOf(
AccountItemPreviewData.accountItem,
AccountItemPreviewData.accountItem.copy(iconState = AccountItemPreviewData.accountLetterIcon),
).map { AccountSelectorItemUM.Account(it, false) }
val first = AccountSelectorUM(
items = firstList.toImmutableList(),
isSingleWallet = false,
)
val second = AccountSelectorUM(
items = secondList.toImmutableList(),
isSingleWallet = true,
)
add(first)
add(second)
},
)

View file

@ -13,14 +13,13 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.account.impl.R
import com.tangem.features.account.selector.entity.AccountSelectorUM
import com.tangem.features.account.selector.entity.PortfolioSelectorUM
@Composable
internal fun AccountSelectorBS(state: AccountSelectorUM, onDismiss: () -> Unit, modifier: Modifier = Modifier) {
internal fun PortfolioSelectorBS(state: PortfolioSelectorUM, onDismiss: () -> Unit, modifier: Modifier = Modifier) {
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
@ -32,13 +31,13 @@ internal fun AccountSelectorBS(state: AccountSelectorUM, onDismiss: () -> Unit,
containerColor = TangemTheme.colors.background.secondary,
title = {
TangemModalBottomSheetTitle(
title = resourceReference(R.string.common_choose_account),
title = state.title,
startIconRes = R.drawable.ic_back_24,
onStartClick = onDismiss,
)
},
content = {
AccountSelectorContent(
PortfolioSelectorContent(
state = state,
contentPadding = PaddingValues(bottom = 16.dp),
modifier = modifier.padding(horizontal = 16.dp),
@ -50,9 +49,9 @@ internal fun AccountSelectorBS(state: AccountSelectorUM, onDismiss: () -> Unit,
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview(@PreviewParameter(AccountSelectorPreviewStateProvider::class) params: AccountSelectorUM) {
private fun Preview(@PreviewParameter(PortfolioSelectorPreviewStateProvider::class) params: PortfolioSelectorUM) {
TangemThemePreview {
AccountSelectorBS(
PortfolioSelectorBS(
state = params,
onDismiss = {},
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),

View file

@ -0,0 +1,219 @@
package com.tangem.features.account.selector.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
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.datasource.CollectionPreviewParameterProvider
import com.tangem.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.common.ui.userwallet.state.UserWalletItemUM.ImageState
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.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.account.impl.R
import com.tangem.features.account.selector.entity.PortfolioSelectorItemUM
import com.tangem.features.account.selector.entity.PortfolioSelectorUM
import com.tangem.features.account.selector.ui.PortfolioSelectorPreviewData.firstList
import com.tangem.features.account.selector.ui.PortfolioSelectorPreviewData.lockedWalletList
import com.tangem.features.account.selector.ui.PortfolioSelectorPreviewData.secondList
import com.tangem.features.account.selector.ui.PortfolioSelectorPreviewData.walletList
import kotlinx.collections.immutable.toImmutableList
import java.util.UUID
private const val DISABLED_WALLET_ALPHA = 0.5f
@Composable
internal fun PortfolioSelectorContent(
state: PortfolioSelectorUM,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(),
) {
LazyColumn(
modifier = modifier,
contentPadding = contentPadding,
) {
val items = state.items
itemsIndexed(
items = items,
key = { _, item -> item.id },
) { index, item ->
val previewItem = items.getOrNull(index.dec())
val offsetModifier = when {
previewItem == null -> Modifier
item is PortfolioSelectorItemUM.GroupTitle -> Modifier.padding(
top = TangemTheme.dimens.spacing16,
)
else -> Modifier.padding(
top = TangemTheme.dimens.spacing8,
)
}
when (item) {
is PortfolioSelectorItemUM.Portfolio -> UserWalletItem(
state = item.item,
modifier = offsetModifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.background(color = TangemTheme.colors.background.primary)
.let { if (!item.item.isEnabled) it.alpha(DISABLED_WALLET_ALPHA) else it },
)
is PortfolioSelectorItemUM.GroupTitle -> WalletNameRow(
model = item,
modifier = offsetModifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
}
}
}
@Composable
private fun WalletNameRow(model: PortfolioSelectorItemUM.GroupTitle, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = model.name.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview(@PreviewParameter(PortfolioSelectorPreviewStateProvider::class) params: PortfolioSelectorUM) {
TangemThemePreview {
PortfolioSelectorContent(
state = params,
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
)
}
}
internal object PortfolioSelectorPreviewData {
val accountName get() = stringReference(value = "Portfolio")
val walletName get() = stringReference(value = "Tangem 2.0")
private val accountItem: UserWalletItemUM
get() = UserWalletItemUM(
id = UserWalletId(UUID.randomUUID().toString().encodeToByteArray()),
name = accountName,
information = UserWalletItemUM.Information.Loaded(stringReference("12 tokens")),
balance = UserWalletItemUM.Balance.Loaded("$726.04", false),
isEnabled = true,
onClick = { },
imageState = ImageState.Account(
name = accountName,
icon = AccountIconPreviewData.randomAccountIcon(),
),
label = null,
)
private val walletItem: UserWalletItemUM
get() = UserWalletItemUM(
id = UserWalletId(UUID.randomUUID().toString().encodeToByteArray()),
name = walletName,
information = UserWalletItemUM.Information.Loaded(stringReference("12 tokens")),
balance = UserWalletItemUM.Balance.Loaded("$726.04", false),
isEnabled = true,
onClick = { },
imageState = ImageState.MobileWallet,
label = null,
)
private val lockedWalletItem: UserWalletItemUM
get() = walletItem.copy(isEnabled = false)
val firstList
get() = buildList {
PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = stringReference("Tangem 2.0"),
).let(::add)
accountItem
.let { PortfolioSelectorItemUM.Portfolio(it) }
.let(::add)
accountItem
.let { PortfolioSelectorItemUM.Portfolio(it) }
.let(::add)
PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = stringReference("Tangem White"),
).let(::add)
accountItem.let { PortfolioSelectorItemUM.Portfolio(it) }
.let(::add)
}
val secondList
get() = firstList + buildList {
PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = resourceReference(R.string.common_locked_wallets),
).let(::add)
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
}
val walletList
get() = buildList {
add(PortfolioSelectorItemUM.Portfolio(walletItem))
add(PortfolioSelectorItemUM.Portfolio(walletItem))
}
val lockedWalletList
get() = buildList {
add(PortfolioSelectorItemUM.Portfolio(walletItem))
val title = PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = resourceReference(R.string.common_locked_wallets),
)
add(title)
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
}
}
internal class PortfolioSelectorPreviewStateProvider : CollectionPreviewParameterProvider<PortfolioSelectorUM>(
buildList {
val first = PortfolioSelectorUM(
title = resourceReference(R.string.common_choose_account),
items = firstList.toImmutableList(),
)
val second = PortfolioSelectorUM(
title = resourceReference(R.string.common_choose_account),
items = secondList.toImmutableList(),
)
val walletListUM = PortfolioSelectorUM(
title = resourceReference(R.string.common_choose_wallet),
items = walletList.toImmutableList(),
)
val lockedWalletListUM =
PortfolioSelectorUM(
title = resourceReference(R.string.common_choose_wallet),
items = lockedWalletList.toImmutableList(),
)
add(first)
add(second)
add(walletListUM)
add(lockedWalletListUM)
},
)

View file

@ -13,7 +13,7 @@ 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.card.repository.CardSdkConfigRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.settings.SetAskBiometryShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository

View file

@ -23,8 +23,8 @@ 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.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.error.SaveWalletError
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
@ -61,14 +61,14 @@ internal class CreateWalletSelectionModel @Inject constructor(
) : Model() {
internal val uiState: StateFlow<CreateWalletSelectionUM>
field = MutableStateFlow(
CreateWalletSelectionUM(
onBackClick = { router.pop() },
onMobileWalletClick = ::onMobileWalletClick,
onHardwareWalletClick = ::onHardwareWalletClick,
onScanClick = ::onScanClick,
),
)
field = MutableStateFlow(
CreateWalletSelectionUM(
onBackClick = { router.pop() },
onMobileWalletClick = ::onMobileWalletClick,
onHardwareWalletClick = ::onHardwareWalletClick,
onScanClick = ::onScanClick,
),
)
init {
showAlreadyHaveWalletWithDelay()

View file

@ -140,11 +140,11 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi
@Composable
private fun WalletBlock(
modifier: Modifier = Modifier,
title: String,
description: String,
badge: @Composable () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
badge: @Composable () -> Unit,
) {
Column(
modifier = modifier

View file

@ -18,11 +18,11 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.common.wallets.error.SaveWalletError
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase
import com.tangem.features.details.impl.R

View file

@ -26,8 +26,8 @@ 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.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.error.SaveWalletError
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.redux.ReduxStateHolder
import com.tangem.domain.settings.repositories.SettingsRepository
@ -46,14 +46,7 @@ import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Locale

View file

@ -95,11 +95,11 @@ fun StoriesTextAnimation(
@Composable
fun StoriesBottomImageAnimation(
firstStepDuration: Int,
totalDuration: Int,
initialScale: Float = 2.5f,
secondStageScale: Float = SCALE_SWITCH_BARRIER,
targetScale: Float = 1.0f,
firstStepDuration: Int,
totalDuration: Int,
content: @Composable (Modifier) -> Unit,
) {
val secondStepDuration = totalDuration - firstStepDuration

View file

@ -25,10 +25,7 @@ internal class AccessCodeComponent @AssistedInject constructor(
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
if (!state.isConfirmMode) {
DisableScreenshotsDisposableEffect()
}
DisableScreenshotsDisposableEffect()
AccessCode(
modifier = modifier,
state = state,

View file

@ -6,10 +6,11 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.components.fields.PinTextColor
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.settings.CanUseBiometryUseCase
@ -56,10 +57,11 @@ internal class AccessCodeModel @Inject constructor(
private val params = paramsContainer.require<AccessCodeComponent.Params>()
internal val uiState: StateFlow<AccessCodeUM>
field = MutableStateFlow(getInitialState())
field = MutableStateFlow(getInitialState())
private fun getInitialState() = AccessCodeUM(
accessCode = "",
accessCodeColor = PinTextColor.Primary,
onAccessCodeChange = ::onAccessCodeChange,
isConfirmMode = params.accessCodeToConfirm != null,
buttonEnabled = false,
@ -76,6 +78,12 @@ internal class AccessCodeModel @Inject constructor(
} else {
value.length == uiState.value.accessCodeLength
},
accessCodeColor = when {
params.accessCodeToConfirm == null -> PinTextColor.Primary
value.length != uiState.value.accessCodeLength -> PinTextColor.Primary
value == params.accessCodeToConfirm -> PinTextColor.Success
else -> PinTextColor.WrongCode
},
)
}
}

View file

@ -1,9 +1,11 @@
package com.tangem.features.hotwallet.accesscode.entity
import com.tangem.core.ui.components.fields.PinTextColor
import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH
internal data class AccessCodeUM(
val accessCode: String,
val accessCodeColor: PinTextColor,
val onAccessCodeChange: (String) -> Unit,
val isConfirmMode: Boolean,
val buttonEnabled: Boolean,

View file

@ -76,7 +76,7 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) {
length = state.accessCodeLength,
isPasswordVisual = state.isConfirmMode,
value = state.accessCode,
pinTextColor = PinTextColor.Primary,
pinTextColor = state.accessCodeColor,
onValueChange = state.onAccessCodeChange,
)
}
@ -109,6 +109,7 @@ private fun PreviewSet() {
AccessCode(
state = AccessCodeUM(
accessCode = "",
accessCodeColor = PinTextColor.Primary,
onAccessCodeChange = {},
isConfirmMode = false,
buttonEnabled = false,
@ -127,6 +128,7 @@ private fun PreviewConfirm() {
AccessCode(
state = AccessCodeUM(
accessCode = "123456",
accessCodeColor = PinTextColor.Success,
onAccessCodeChange = {},
isConfirmMode = true,
buttonEnabled = true,

View file

@ -5,7 +5,7 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.FullScreen
import com.tangem.core.ui.components.DialogFullScreen
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
import com.tangem.features.hotwallet.accesscoderequest.ui.HotAccessCodeRequestFullScreenContent
@ -47,7 +47,7 @@ internal class DefaultHotAccessCodeRequestComponent @AssistedInject constructor(
var isShownIfProxy by remember { mutableStateOf(state.isShown) }
if (isShownIfProxy) {
FullScreen(focusable = true, onBackClick = state.onDismiss) {
DialogFullScreen(onDismissRequest = state.onDismiss) {
HotAccessCodeRequestFullScreenContent(
state = state.copy(isShown = isShownProxy),
modifier = modifier,

View file

@ -6,10 +6,11 @@ import com.tangem.core.ui.components.fields.PinTextColor
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
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
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH
import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM
@ -43,7 +44,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
)
val uiState: StateFlow<HotAccessCodeRequestUM>
field = MutableStateFlow(getInitialState())
field = MutableStateFlow(getInitialState())
suspend fun show(attemptRequest: HotWalletPasswordRequester.AttemptRequest) {
if (userWalletExists(attemptRequest.hotWalletId).not()) {
@ -82,6 +83,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
it.copy(
accessCodeColor = PinTextColor.WrongCode,
onAccessCodeChange = {},
useBiometricVisible = currentRequest.hasBiometry,
)
}
delay(timeMillis = 500) // Delay to show the wrong access code state
@ -121,7 +123,10 @@ internal class HotAccessCodeRequestModel @Inject constructor(
if (accessCode.length == ACCESS_CODE_LENGTH) {
uiState.update {
it.copy(onAccessCodeChange = {})
it.copy(
onAccessCodeChange = {},
useBiometricVisible = false,
)
}
result.value = HotWalletPasswordRequester.Result.EnteredPassword(HotAuth.Password(accessCode.toCharArray()))
@ -143,7 +148,24 @@ internal class HotAccessCodeRequestModel @Inject constructor(
suspend fun collectAttempts(attempts: Attempts) {
when (attempts) {
is Attempts.FastForward -> {
/** ignore */
if (attempts.count > 0) {
uiState.update {
it.copy(
wrongAccessCodeText = resourceReference(
R.string.access_code_check_warining_lock,
wrappedList(MAX_FAST_FORWARD_ATTEMPTS - attempts.count),
),
onAccessCodeChange = ::onAccessCodeChange,
)
}
} else {
uiState.update {
it.copy(
wrongAccessCodeText = null,
onAccessCodeChange = ::onAccessCodeChange,
)
}
}
}
is Attempts.WithDelay -> {
uiState.update {

View file

@ -37,105 +37,110 @@ import com.tangem.features.hotwallet.impl.R
@Suppress("MagicNumber", "LongMethod")
@Composable
internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM, modifier: Modifier = Modifier) {
AnimatedVisibility(
modifier = modifier,
visible = state.isShown,
enter = fadeIn(),
exit = fadeOut(),
) {
Column(
Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
horizontalAlignment = Alignment.CenterHorizontally,
Box(Modifier.fillMaxSize()) {
AnimatedVisibility(
modifier = modifier,
visible = state.isShown,
enter = fadeIn(),
exit = fadeOut(),
) {
TangemTopAppBar(
modifier = Modifier
.statusBarsPadding(),
startButton = TopAppBarButtonUM.Back(state.onDismiss),
)
SpacerH(68.dp)
Column(
Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 24.dp),
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
text = stringResourceSafe(R.string.access_code_check_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
TangemTopAppBar(
modifier = Modifier.statusBarsPadding(),
startButton = TopAppBarButtonUM.Back(state.onDismiss),
)
SpacerH24()
SpacerH(68.dp)
PinTextField(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
length = 6,
isPasswordVisual = true,
value = state.accessCode,
pinTextColor = state.accessCodeColor,
onValueChange = state.onAccessCodeChange,
)
Column(
Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
text = stringResourceSafe(R.string.access_code_check_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
SpacerH(20.dp)
SpacerH24()
PinTextField(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
length = 6,
isPasswordVisual = true,
value = state.accessCode,
pinTextColor = state.accessCodeColor,
onValueChange = state.onAccessCodeChange,
)
SpacerH(20.dp)
AnimatedVisibility(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
visible = state.wrongAccessCodeText != null,
enter = fadeIn(),
exit = fadeOut(),
) {
val wrongAccessCodeText =
state.wrongAccessCodeText ?: return@AnimatedVisibility
Text(
text = wrongAccessCodeText.resolveReference(),
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2.copy(
lineBreak = LineBreak.Heading,
),
color = TangemTheme.colors.text.warning,
)
}
}
AnimatedVisibility(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
visible = state.wrongAccessCodeText != null,
visible = state.useBiometricVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
val wrongAccessCodeText =
state.wrongAccessCodeText ?: return@AnimatedVisibility
Text(
text = wrongAccessCodeText.resolveReference(),
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2.copy(
lineBreak = LineBreak.Heading,
SecondaryButton(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.navigationBarsPadding()
.imePadding(),
text = stringResourceSafe(
id = R.string.welcome_unlock,
stringResourceSafe(R.string.common_biometrics),
),
color = TangemTheme.colors.text.warning,
onClick = state.useBiometricClick,
)
}
}
if (state.useBiometricVisible) {
SecondaryButton(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.navigationBarsPadding()
.imePadding(),
text = stringResourceSafe(
id = R.string.welcome_unlock,
stringResourceSafe(R.string.common_biometrics),
),
onClick = state.useBiometricClick,
)
}
}
}

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.hotwallet.addexistingwallet.im.port.model.AddExistingWalletImportModel
import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.AddExistingWalletImportContent
@ -22,6 +23,7 @@ internal class AddExistingWalletImportComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
DisableScreenshotsDisposableEffect()
AddExistingWalletImportContent(
state = state,
modifier = modifier,

View file

@ -6,16 +6,12 @@ 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.R
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
import com.tangem.core.ui.components.bottomsheets.message.icon
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
import com.tangem.core.ui.components.bottomsheets.message.onClick
import com.tangem.core.ui.components.bottomsheets.message.secondaryButton
import com.tangem.core.ui.components.bottomsheets.message.*
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.common.wallets.error.SaveWalletError
import com.tangem.domain.wallets.builder.HotUserWalletBuilder
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.hotwallet.MnemonicRepository
@ -80,7 +76,7 @@ internal class AddExistingWalletImportModel @Inject constructor(
}
internal val uiState: StateFlow<AddExistingWalletImportUM>
field = MutableStateFlow(importSeedPhraseUiStateBuilder.getState())
field = MutableStateFlow(importSeedPhraseUiStateBuilder.getState())
@Suppress("UnusedPrivateMember")
private fun importWallet(mnemonic: Mnemonic, passphrase: String?) {

View file

@ -67,13 +67,17 @@ internal class ImportSeedPhraseUiStateBuilder(
val text = st.words.text
val wordsFromText = text.split(" ").filter { it.isNotBlank() }.map { it.trim() }
val newWords = wordsFromText.dropLast(1) + word
val newWordsText = newWords.joinToString(" ")
st.copy(
words = TextFieldValue(
text = newWordsText,
selection = TextRange(newWordsText.length),
),
val newWordsText = newWords.joinToString(" ").plus(" ")
val newWordsState = TextFieldValue(
text = newWordsText,
selection = TextRange(newWordsText.length),
)
st.copy(
words = newWordsState,
).also {
launchInterceptWords(wordsField = newWordsState)
suggestNextWord(newWordsState)
}
}
}

View file

@ -23,8 +23,8 @@ 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.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.error.SaveWalletError
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
@ -63,16 +63,16 @@ internal class AddExistingWalletStartModel @Inject constructor(
private val params: AddExistingWalletStartComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<AddExistingWalletStartUM>
field = MutableStateFlow(
AddExistingWalletStartUM(
showWantToPurchaseBlock = false,
isScanInProgress = false,
onBackClick = params.callbacks::onBackClick,
onImportPhraseClick = params.callbacks::onImportPhraseClick,
onScanCardClick = ::onScanClick,
onBuyCardClick = ::onShopClick,
),
)
field = MutableStateFlow(
AddExistingWalletStartUM(
showWantToPurchaseBlock = false,
isScanInProgress = false,
onBackClick = params.callbacks::onBackClick,
onImportPhraseClick = params.callbacks::onImportPhraseClick,
onScanCardClick = ::onScanClick,
onBuyCardClick = ::onShopClick,
),
)
init {
showWantToPurchaseBlockWithDelay()

View file

@ -28,13 +28,13 @@ internal class CreateMobileWalletModel @Inject constructor(
) : Model() {
internal val uiState: StateFlow<CreateMobileWalletUM>
field = MutableStateFlow(
CreateMobileWalletUM(
onBackClick = { router.pop() },
onCreateClick = ::onCreateClick,
createButtonLoading = false,
),
)
field = MutableStateFlow(
CreateMobileWalletUM(
onBackClick = { router.pop() },
onCreateClick = ::onCreateClick,
createButtonLoading = false,
),
)
private fun onCreateClick() {
modelScope.launch {

View file

@ -14,7 +14,6 @@ import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckCompone
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent
import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
@ -28,7 +27,6 @@ internal class CreateWalletBackupModel @Inject constructor(
val params = paramsContainer.require<CreateWalletBackupComponent.Params>()
val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback()
val manualBackupStartModelCallbacks = ManualBackupStartModelCallbacks()
val manualBackupPhraseModelCallbacks = ManualBackupPhraseModelCallbacks()
val manualBackupCheckModelCallbacks = ManualBackupCheckModelCallbacks()
@ -63,14 +61,6 @@ internal class CreateWalletBackupModel @Inject constructor(
router.pop()
}
inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback {
override fun onBackClick() {
onBack()
}
override fun onSkipClick() = Unit
}
inner class ManualBackupStartModelCallbacks : ManualBackupStartComponent.ModelCallbacks {
override fun onContinueClick() {
onManualBackupStarted()

View file

@ -1,56 +0,0 @@
package com.tangem.features.hotwallet.createwalletbackup
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute
import com.tangem.features.hotwallet.impl.R
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import javax.inject.Inject
internal class CreateWalletBackupStepperStateManager @Inject constructor() {
fun getStepperState(route: CreateWalletBackupRoute): HotWalletStepperComponent.StepperUM? {
return when (route) {
is CreateWalletBackupRoute.RecoveryPhraseStart -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_START,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_backup),
showBackButton = true,
showSkipButton = false,
showFeedbackButton = true,
)
is CreateWalletBackupRoute.RecoveryPhrase -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_PHRASE,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_backup),
showBackButton = true,
showSkipButton = false,
showFeedbackButton = true,
)
is CreateWalletBackupRoute.ConfirmBackup -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_CONFIRM,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_backup),
showBackButton = true,
showSkipButton = false,
showFeedbackButton = true,
)
is CreateWalletBackupRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM(
currentStep = STEP_COMPLETED,
steps = STEPS_COUNT,
title = resourceReference(R.string.common_done),
showBackButton = false,
showSkipButton = false,
showFeedbackButton = false,
)
}
}
companion object {
private const val STEPS_COUNT = 4
private const val STEP_START = 1
private const val STEP_PHRASE = 2
private const val STEP_CONFIRM = 3
private const val STEP_COMPLETED = 4
}
}

View file

@ -13,9 +13,8 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.hotwallet.CreateWalletBackupComponent
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupChildFactory
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute
import com.tangem.features.hotwallet.createwalletbackup.ui.CreateWalletBackupContent
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -24,9 +23,7 @@ import kotlinx.coroutines.launch
internal class DefaultCreateWalletBackupComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: CreateWalletBackupComponent.Params,
private val stepperStateManager: CreateWalletBackupStepperStateManager,
createWalletBackupChildFactory: CreateWalletBackupChildFactory,
stepperComponentFactory: DefaultHotWalletStepperComponent.Factory,
) : CreateWalletBackupComponent, AppComponentContext by appComponentContext {
private val model: CreateWalletBackupModel = getOrCreateModel(params)
@ -46,14 +43,6 @@ internal class DefaultCreateWalletBackupComponent @AssistedInject constructor(
},
)
private val stepperComponent = stepperComponentFactory.create(
context = this,
params = HotWalletStepperComponent.Params(
initState = HotWalletStepperComponent.StepperUM.initialState(),
callback = model.hotWalletStepperComponentModelCallback,
),
)
init {
innerStack.subscribe(
lifecycle = lifecycle,
@ -72,13 +61,11 @@ internal class DefaultCreateWalletBackupComponent @AssistedInject constructor(
BackHandler(onBack = model::onBack)
val stepperState = stepperStateManager.getStepperState(currentRoute)
stepperState?.let { stepperComponent.updateState(it) }
CreateWalletBackupContent(
stackState = stackState,
stepperComponent = stepperComponent.takeIf { stepperState != null },
modifier = modifier,
showTopBar = currentRoute !is CreateWalletBackupRoute.BackupCompleted,
onBackClick = model::onBack,
)
}

View file

@ -3,11 +3,9 @@ package com.tangem.features.hotwallet.createwalletbackup.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.CreateWalletBackupComponent
import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupModel
import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupStepperStateManager
import com.tangem.features.hotwallet.createwalletbackup.DefaultCreateWalletBackupComponent
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
@ -16,7 +14,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface CreateWalletBackupModuleBinds {
internal interface CreateWalletBackupModule {
@Binds
@Singleton
@ -28,15 +26,4 @@ internal interface CreateWalletBackupModuleBinds {
@IntoMap
@ClassKey(CreateWalletBackupModel::class)
fun bindCreateWalletBackupModel(model: CreateWalletBackupModel): Model
}
@Module
@InstallIn(SingletonComponent::class)
internal object CreateWalletBackupModule {
@Provides
@Singleton
fun provideCreateWalletBackupStepperStateManager(): CreateWalletBackupStepperStateManager {
return CreateWalletBackupStepperStateManager()
}
}

View file

@ -11,15 +11,19 @@ import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.features.hotwallet.impl.R
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
@Composable
internal fun CreateWalletBackupContent(
stackState: ChildStack<CreateWalletBackupRoute, ComposableContentComponent>,
stepperComponent: HotWalletStepperComponent?,
showTopBar: Boolean,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@ -29,7 +33,15 @@ internal fun CreateWalletBackupContent(
.imePadding()
.systemBarsPadding(),
) {
stepperComponent?.Content(Modifier)
if (showTopBar) {
TangemTopAppBar(
modifier = Modifier,
startButton = TopAppBarButtonUM.Back(
onBackClicked = onBackClick,
),
title = stringResourceSafe(id = R.string.common_backup),
)
}
Children(
stack = stackState,

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.hotwallet.manualbackup.check.model.ManualBackupCheckModel
import com.tangem.features.hotwallet.manualbackup.check.ui.ManualBackupCheckContent
@ -22,6 +23,7 @@ internal class ManualBackupCheckComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
DisableScreenshotsDisposableEffect()
ManualBackupCheckContent(
state = state,
modifier = modifier,

View file

@ -44,7 +44,7 @@ internal class ManualBackupCheckModel @Inject constructor(
private val callbacks = params.callbacks
internal val uiState: StateFlow<ManualBackupCheckUM>
field = MutableStateFlow(getInitialUIState())
field = MutableStateFlow(getInitialUIState())
init {
modelScope.launch {

View file

@ -18,9 +18,9 @@ internal class ManualBackupCompletedModel @Inject constructor(
private val params: ManualBackupCompletedComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<ManualBackupCompletedUM>
field = MutableStateFlow(
ManualBackupCompletedUM(
onContinueClick = { params.callbacks.onContinueClick(params.userWalletId) },
),
)
field = MutableStateFlow(
ManualBackupCompletedUM(
onContinueClick = { params.callbacks.onContinueClick(params.userWalletId) },
),
)
}

View file

@ -35,11 +35,11 @@ internal class ManualBackupPhraseModel @Inject constructor(
private val callbacks = params.callbacks
internal val uiState: StateFlow<ManualBackupPhraseUM>
field = MutableStateFlow(
ManualBackupPhraseUM(
onContinueClick = callbacks::onContinueClick,
),
)
field = MutableStateFlow(
ManualBackupPhraseUM(
onContinueClick = callbacks::onContinueClick,
),
)
init {
modelScope.launch {

View file

@ -18,9 +18,9 @@ internal class ManualBackupStartModel @Inject constructor(
private val params: ManualBackupStartComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<ManualBackupStartUM>
field = MutableStateFlow(
ManualBackupStartUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
field = MutableStateFlow(
ManualBackupStartUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
}

View file

@ -18,9 +18,9 @@ internal class MobileWalletSetupFinishedModel @Inject constructor(
private val params: MobileWalletSetupFinishedComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<MobileWalletSetupFinishedUM>
field = MutableStateFlow(
MobileWalletSetupFinishedUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
field = MutableStateFlow(
MobileWalletSetupFinishedUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
}

View file

@ -20,7 +20,7 @@ internal class HotWalletStepperModel @Inject constructor(
val params = paramsContainer.require<HotWalletStepperComponent.Params>()
val uiState: StateFlow<HotWalletStepperComponent.StepperUM>
field = MutableStateFlow(params.initState)
field = MutableStateFlow(params.initState)
fun updateState(newState: HotWalletStepperComponent.StepperUM) {
uiState.value = newState

View file

@ -35,11 +35,11 @@ internal class ViewPhraseModel @Inject constructor(
private val params = paramsContainer.require<ViewPhraseComponent.Params>()
internal val uiState: StateFlow<ViewPhraseUM>
field = MutableStateFlow(
ViewPhraseUM(
onBackClick = { router.pop() },
),
)
field = MutableStateFlow(
ViewPhraseUM(
onBackClick = { router.pop() },
),
)
init {
loadSeedPhrase()

View file

@ -63,7 +63,7 @@ internal class WalletActivationModel @Inject constructor(
is WalletActivationRoute.ManualBackupCheck -> stackNavigation.pop()
is WalletActivationRoute.ManualBackupCompleted -> Unit
is WalletActivationRoute.SetAccessCode -> Unit
is WalletActivationRoute.ConfirmAccessCode -> Unit
is WalletActivationRoute.ConfirmAccessCode -> stackNavigation.pop()
is WalletActivationRoute.PushNotifications -> Unit
is WalletActivationRoute.SetupFinished -> Unit
}

View file

@ -24,7 +24,7 @@ import javax.inject.Inject
@ModelScoped
internal class WalletBackupModel @Inject constructor(
paramsContainer: ParamsContainer,
private val getWalletUseCase: GetUserWalletUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
@ -33,35 +33,37 @@ internal class WalletBackupModel @Inject constructor(
private val params: WalletBackupComponent.Params = paramsContainer.require()
val uiState: StateFlow<WalletBackupUM>
field = MutableStateFlow(
WalletBackupUM(
onBackClick = { router.pop() },
recoveryPhraseOption = LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
field = MutableStateFlow(
WalletBackupUM(
onBackClick = { router.pop() },
recoveryPhraseOption = LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
),
googleDriveOption = LabelUM(
text = resourceReference(R.string.common_coming_soon),
style = LabelStyle.REGULAR,
),
googleDriveStatus = BackupStatus.ComingSoon,
onRecoveryPhraseClick = ::onRecoveryPhraseClick,
onGoogleDriveClick = { },
onHardwareWalletClick = ::onHardwareWalletClick,
backedUp = false,
),
googleDriveOption = LabelUM(
text = resourceReference(R.string.common_coming_soon),
style = LabelStyle.REGULAR,
),
googleDriveStatus = BackupStatus.ComingSoon,
onRecoveryPhraseClick = ::onRecoveryPhraseClick,
onGoogleDriveClick = { },
onHardwareWalletClick = ::onHardwareWalletClick,
backedUp = false,
),
)
)
init {
getWalletUseCase.invoke(params.userWalletId)
.fold(
ifLeft = {
Timber.e("Error on getting user wallet: $it")
},
ifRight = {
updateBackupStatuses(it)
},
)
getUserWalletUseCase.invokeFlow(params.userWalletId)
.onEach { either ->
either.fold(
ifLeft = {
Timber.e("Error on getting user wallet: $it")
},
ifRight = {
updateBackupStatuses(it)
},
)
}.launchIn(modelScope)
}
private fun updateBackupStatuses(userWallet: UserWallet) {
@ -95,7 +97,7 @@ internal class WalletBackupModel @Inject constructor(
private fun onRecoveryPhraseClick() {
if (uiState.value.backedUp) {
getWalletUseCase.invoke(params.userWalletId)
getUserWalletUseCase.invoke(params.userWalletId)
.fold(
ifLeft = {
Timber.e("Error on getting user wallet: $it")

View file

@ -1,12 +1,11 @@
package com.tangem.features.kyc
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface KycComponent {
interface KycComponent : ComposableContentComponent {
fun launch()
data object Params
interface Factory {
fun create(appComponentContext: AppComponentContext): KycComponent
}
interface Factory : ComponentFactory<Params, KycComponent>
}

View file

@ -1,45 +1,62 @@
package com.tangem.features.kyc
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.sumsub.sns.core.SNSMobileSDK
import com.sumsub.sns.core.data.listener.TokenExpirationHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.domain.pay.KycStartInfo
import com.tangem.features.kyc.theme.TangemSNSIconHandler
import com.tangem.features.kyc.theme.TangemSNSTheme
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import java.util.Locale
class DefaultKycComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: KycComponent.Params,
) : KycComponent, AppComponentContext by appComponentContext {
private val model: DefaultKycModel = getOrCreateModel()
private val model: DefaultKycModel = getOrCreateModel(params)
override fun launch() {
init {
componentScope.launch {
model.uiState.collect {
it?.let { startInfo ->
val tokenExpirationHandler = object : TokenExpirationHandler {
override fun onTokenExpired() = ""
}
val snsSdk = SNSMobileSDK.Builder(activity)
.withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler)
.withTheme(TangemSNSTheme.theme(activity))
.withIconHandler(TangemSNSIconHandler())
.withLocale(Locale(startInfo.locale))
.build()
snsSdk.launch()
}
model.uiState.drop(1).collectLatest { startInfo ->
startInfo?.let { launchSdk(startInfo) }
router.pop()
}
}
model.getKycToken()
}
private fun launchSdk(startInfo: KycStartInfo) {
val tokenExpirationHandler = object : TokenExpirationHandler {
/**
* We don't refresh this token for f&f release. Assume it lives long enough to finish KYC
* [REDACTED_TODO_COMMENT]
*/
override fun onTokenExpired() = ""
}
val snsSdk = SNSMobileSDK.Builder(activity)
.withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler)
.withTheme(TangemSNSTheme.theme(activity))
.withIconHandler(TangemSNSIconHandler())
.withLocale(Locale(startInfo.locale))
.build()
snsSdk.launch()
}
@Composable
override fun Content(modifier: Modifier) {
KycLoadingScreen(router::pop, modifier)
}
@AssistedFactory
interface Factory : KycComponent.Factory {
override fun create(appComponentContext: AppComponentContext): DefaultKycComponent
override fun create(context: AppComponentContext, params: KycComponent.Params): DefaultKycComponent
}
}

View file

@ -21,7 +21,7 @@ class DefaultKycModel @Inject constructor(
private val _uiState: MutableStateFlow<KycStartInfo?> = MutableStateFlow(null)
val uiState = _uiState.asStateFlow()
fun getKycToken() {
init {
modelScope.launch {
kycRepository.getKycStartInfo().getOrNull()?.let { _uiState.emit(it) }
}

View file

@ -0,0 +1,41 @@
package com.tangem.features.kyc
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.tangem.core.ui.R
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun KycLoadingScreen(onBack: () -> Unit, modifier: Modifier = Modifier) {
Scaffold(
modifier = modifier,
topBar = {
AppBarWithBackButton(
modifier = Modifier.statusBarsPadding(),
onBackClick = onBack,
iconRes = R.drawable.ic_back_24,
)
},
content = { paddingValues ->
Box(
modifier = modifier
.padding(paddingValues)
.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
modifier = Modifier,
color = TangemTheme.colors.icon.primary1,
)
}
},
)
}

View file

@ -15,6 +15,8 @@ dependencies {
implementation(projects.features.kyc.api)
implementation(projects.core.decompose)
implementation(deps.compose.ui)
implementation(projects.core.ui)
/** DI */
implementation(deps.hilt.android)

View file

@ -1,5 +1,7 @@
package com.tangem.features.kyc
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -7,17 +9,17 @@ import dagger.assisted.AssistedInject
/**
* Mocking it for release/external builds to exclude SumSub dependency
* This will never be called if the FT [isTangemPayEnabled] is off
*/
internal class MockKycComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
) : KycComponent, AppComponentContext by appComponentContext {
override fun launch() {
/* no op */
}
@Composable
override fun Content(modifier: Modifier) { /* no op */ }
@AssistedFactory
interface Factory : KycComponent.Factory {
override fun create(appComponentContext: AppComponentContext): MockKycComponent
override fun create(context: AppComponentContext, params: KycComponent.Params): MockKycComponent
}
}

View file

@ -81,7 +81,7 @@ internal class ChooseManagedTokensModel @Inject constructor(
val bottomSheetNavigation: SlotNavigation<ChooseManageTokensBottomSheetConfig> = SlotNavigation()
val uiState: StateFlow<ChooseManagedTokenUM>
field = MutableStateFlow<ChooseManagedTokenUM>(createReadContentModel())
field = MutableStateFlow<ChooseManagedTokenUM>(createReadContentModel())
init {
manageTokensListManager.uiItems

View file

@ -83,6 +83,11 @@ internal class ManageTokensListManager @AssistedInject constructor(
.distinctUntilChanged()
val uiItems: Flow<ImmutableList<CurrencyItemUM>> = uiManager.items
/**
* Launch pagination flow to get currencies
*
* @param isCollapsed set initial display state of networks. !!! WARNING !!! Use `false` flag with cation
*/
suspend fun launchPagination(isCollapsed: Boolean) = coroutineScope {
val loadUserTokensFromRemote = when (mode) {
is ManageTokensMode.Wallet -> source == ManageTokensSource.ONBOARDING

View file

@ -58,8 +58,8 @@ internal fun MarketsTokenDetailsContent(
onBackClick: () -> Unit,
onHeaderSizeChange: (Dp) -> Unit,
backButtonEnabled: Boolean,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
modifier: Modifier = Modifier,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
) {
Content(
modifier = modifier,
@ -88,8 +88,8 @@ private fun Content(
onBackClick: () -> Unit,
onHeaderSizeChange: (Dp) -> Unit,
backButtonEnabled: Boolean,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
modifier: Modifier = Modifier,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
) {
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier
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.core.ui.components.icons.IconTint
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.haptic.TangemHapticEffect
@ -100,9 +101,9 @@ private class PortfolioTokenUMProvider : CollectionPreviewParameterProvider<Port
tokenItemState = (tokenUM.tokenItemState as TokenItemState.Content).copy(
fiatAmountState = contentFiatAmount.copy(
icons = persistentListOf(
TokenItemState.FiatAmountState.Content.IconUM(
TokenFiatAmountState.Content.IconUM(
iconRes = R.drawable.ic_staking_24,
useAccentColor = true,
tint = IconTint.Accent,
),
),
),

View file

@ -185,9 +185,9 @@ internal fun NFTDetailsGroupBlock(
@Composable
internal fun NFTBlocksGroupAction(
text: TextReference,
startIcon: @Composable RowScope.() -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
startIcon: @Composable RowScope.() -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }

View file

@ -17,7 +17,7 @@ interface NFTSendSuccessListener {
internal class DefaultNFTSendSuccessTrigger @Inject constructor() : NFTSendSuccessTrigger, NFTSendSuccessListener {
override val nftSendSuccessFlow: SharedFlow<Unit>
field = MutableSharedFlow<Unit>()
field = MutableSharedFlow<Unit>()
override suspend fun triggerSuccessNFTSend() {
nftSendSuccessFlow.emit(Unit)

View file

@ -4,7 +4,6 @@ import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.replaceAll
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.models.scan.ProductType
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -13,12 +12,13 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.onboarding.v2.TitleProvider
@ -219,7 +219,7 @@ internal class OnboardingEntryModel @Inject constructor(
// legacy flow
if (userWalletsListManager.hasUserWallets) {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false }
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLocked!! }.getOrElse { false }
if (isLocked) {
router.replaceAll(AppRoute.Welcome())

View file

@ -14,8 +14,8 @@ import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute
@Composable
internal inline fun OnboardingEntry(
modifier: Modifier = Modifier,
childStack: ChildStack<OnboardingRoute, Any>,
modifier: Modifier = Modifier,
stepperContent: @Composable (Modifier) -> Unit,
) {
Column(

View file

@ -7,6 +7,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -19,6 +20,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StoriesScreenTestTags
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM
@ -60,7 +62,9 @@ internal fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier:
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 16.dp),
modifier = Modifier
.padding(top = 16.dp)
.testTag(StoriesScreenTestTags.TITLE),
)
Text(
@ -68,7 +72,9 @@ internal fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier:
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 12.dp),
modifier = Modifier
.padding(top = 12.dp)
.testTag(StoriesScreenTestTags.TEXT),
)
}

View file

@ -58,13 +58,17 @@ internal class ImportSeedPhraseUiStateBuilder(
val text = st.words.text
val wordsFromText = text.split(" ").filter { it.isNotBlank() }.map { it.trim() }
val newWords = wordsFromText.dropLast(1) + word
val newWordsText = newWords.joinToString(" ")
st.copy(
words = TextFieldValue(
text = newWordsText,
selection = TextRange(newWordsText.length),
),
val newWordsText = newWords.joinToString(" ").plus(" ")
val newWordsState = TextFieldValue(
text = newWordsText,
selection = TextRange(newWordsText.length),
)
st.copy(
words = newWordsState,
).also {
launchInterceptWords(wordsField = newWordsState)
suggestNextWord(newWordsState)
}
}
}

View file

@ -16,6 +16,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -25,6 +26,7 @@ import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SelectPaymentMethodBottomSheetTestTags
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.PaymentMethodType
import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM
@ -74,10 +76,13 @@ private fun PaymentMethod(methodUM: AllOffersPaymentMethodUM, modifier: Modifier
end = 12.dp,
top = 14.dp,
bottom = 12.dp,
),
)
.testTag(SelectPaymentMethodBottomSheetTestTags.PAYMENT_METHOD),
) {
PaymentMethodIcon(
modifier = Modifier.size(36.dp),
modifier = Modifier
.size(36.dp)
.testTag(SelectPaymentMethodBottomSheetTestTags.PAYMENT_METHOD_ICON),
imageUrl = methodUM.methodConfig.method.imageUrl,
)
@ -111,6 +116,7 @@ private fun PaymentMethodInfoBlock(
text = paymentMethodName,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.testTag(SelectPaymentMethodBottomSheetTestTags.PAYMENT_METHOD_NAME),
)
SpacerH(2.dp)
Row(
@ -121,12 +127,14 @@ private fun PaymentMethodInfoBlock(
text = stringResourceSafe(R.string.onramp_up_to_rate),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.testTag(SelectPaymentMethodBottomSheetTestTags.UP_TO_TEXT),
)
Text(
text = rate,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.testTag(SelectPaymentMethodBottomSheetTestTags.TOKEN_AMOUNT),
)
when {
@ -134,6 +142,7 @@ private fun PaymentMethodInfoBlock(
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_best_rate_12),
contentDescription = null,
modifier = Modifier.testTag(SelectPaymentMethodBottomSheetTestTags.BEST_RATE_ICON),
)
}
diff != null -> {
@ -158,7 +167,9 @@ private fun PaymentMethodInfoBlock(
private fun ProvidersCountBlockInfo(providersCount: Int) {
BorderedRow {
Icon(
modifier = Modifier.size(10.dp),
modifier = Modifier
.size(10.dp)
.testTag(SelectPaymentMethodBottomSheetTestTags.PROVIDER_COUNT_ICON),
imageVector = ImageVector.vectorResource(R.drawable.ic_clock_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
@ -172,6 +183,7 @@ private fun ProvidersCountBlockInfo(providersCount: Int) {
),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.testTag(SelectPaymentMethodBottomSheetTestTags.PROVIDER_COUNT_TEXT),
)
}
}
@ -180,7 +192,9 @@ private fun ProvidersCountBlockInfo(providersCount: Int) {
private fun TimingBlockInfo(speed: PaymentMethodType.PaymentSpeed) {
BorderedRow {
Icon(
modifier = Modifier.size(10.dp),
modifier = Modifier
.size(10.dp)
.testTag(SelectPaymentMethodBottomSheetTestTags.TIMING_ICON),
imageVector = ImageVector.vectorResource(R.drawable.ic_staking_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,

View file

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.ClickableText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@ -16,7 +15,6 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.extensions.appendColored
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM
@ -79,7 +77,6 @@ private fun OnrampTosText(provider: OnrampProviderBlockUM.Content?) {
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
),
modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.TOS_BLOCK),
onClick = { offset ->
clickableAnnotation.getStringAnnotations(
tag = TERMS_OF_USE_KEY,

View file

@ -13,14 +13,12 @@ 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.platform.testTag
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import com.tangem.core.ui.extensions.appendSpace
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM
import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon
@ -62,7 +60,6 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier:
},
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_TITLE),
)
Text(
text = buildAnnotatedString {
@ -72,7 +69,6 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier:
},
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_TEXT),
)
}
AnimatedVisibility(
@ -112,7 +108,6 @@ private fun OnrampProviderLoading(modifier: Modifier = Modifier) {
text = stringResourceSafe(id = R.string.express_provider),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_LOADING_TITLE),
)
Row(
verticalAlignment = Alignment.CenterVertically,
@ -127,7 +122,6 @@ private fun OnrampProviderLoading(modifier: Modifier = Modifier) {
text = stringResourceSafe(id = R.string.express_fetch_best_rates),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_LOADING_TEXT),
)
}
}

View file

@ -19,6 +19,7 @@ import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -30,6 +31,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.OnrampOffersBlockTestTags
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.PaymentMethodType
import com.tangem.features.onramp.impl.R
@ -156,11 +158,13 @@ private fun OfferHeader(advantage: OnrampOfferAdvantagesUM) {
imageVector = ImageVector.vectorResource(R.drawable.ic_best_rate_16),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
modifier = Modifier.testTag(OnrampOffersBlockTestTags.BEST_RATE_ICON),
)
Text(
text = stringResourceSafe(R.string.express_provider_best_rate),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.icon.accent,
modifier = Modifier.testTag(OnrampOffersBlockTestTags.BEST_RATE_TITLE),
)
}
}
@ -194,6 +198,7 @@ private fun RateBlock(rate: String, diff: TextReference?) {
text = rate,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.testTag(OnrampOffersBlockTestTags.OFFER_TOKEN_AMOUNT),
)
diff?.let {
Text(
@ -215,7 +220,9 @@ private fun RateBlock(rate: String, diff: TextReference?) {
private fun PaymentBlockInOffer(paymentMethod: OnrampPaymentMethod, providerName: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
modifier = Modifier.size(16.dp),
modifier = Modifier
.size(16.dp)
.testTag(OnrampOffersBlockTestTags.TIMING_ICON),
imageVector = ImageVector.vectorResource(R.drawable.ic_clock_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
@ -235,6 +242,7 @@ private fun PaymentBlockInOffer(paymentMethod: OnrampPaymentMethod, providerName
text = providerName,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.testTag(OnrampOffersBlockTestTags.PROVIDER_NAME),
)
SpacerWMax()
@ -243,12 +251,15 @@ private fun PaymentBlockInOffer(paymentMethod: OnrampPaymentMethod, providerName
text = stringResourceSafe(R.string.onramp_pay_with),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.testTag(OnrampOffersBlockTestTags.PAY_WITH),
)
SpacerW(4.dp)
SubcomposeAsyncImage(
modifier = Modifier.sizeIn(maxWidth = 38.dp, maxHeight = 16.dp),
modifier = Modifier
.sizeIn(maxWidth = 38.dp, maxHeight = 16.dp)
.testTag(OnrampOffersBlockTestTags.PAYMENT_METHOD_ICON),
model = ImageRequest.Builder(context = LocalContext.current)
.data(paymentMethod.imageUrl)
.crossfade(enable = true)
@ -298,6 +309,7 @@ internal fun TimingBlock(speed: PaymentMethodType.PaymentSpeed) {
text = timingText,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.testTag(OnrampOffersBlockTestTags.TIMING_TEXT),
)
}

View file

@ -11,13 +11,11 @@ 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.platform.testTag
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.selectedBorder
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.SelectPaymentMethodBottomSheetTestTags
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodUM
import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodsBottomSheetConfig
@ -51,7 +49,7 @@ private fun SelectPaymentMethodBottomSheetContent(
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier.testTag(SelectPaymentMethodBottomSheetTestTags.LAZY_LIST),
modifier = modifier,
) {
items(
items = methods,
@ -90,7 +88,6 @@ private fun PaymentMethodItem(paymentMethod: PaymentMethodUM, isSelected: Boolea
) {
PaymentMethodIcon(
imageUrl = paymentMethod.imageUrl,
modifier = Modifier.testTag(SelectPaymentMethodBottomSheetTestTags.PAYMENT_METHOD_ICON),
)
Text(
text = paymentMethod.name,

View file

@ -9,5 +9,4 @@ internal enum class OnrampOperation {
BUY,
SELL,
SWAP,
;
}

View file

@ -53,20 +53,18 @@ internal fun PushNotificationsContent(
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
ShowcaseContent(
headerIconRes = R.drawable.ic_notifications_unread_24,
headerIconRes = R.drawable.ic_notification_56,
headerText = resourceReference(R.string.user_push_notification_agreement_header),
showcaseItems = persistentListOf(
ShowcaseItemModel(
R.drawable.ic_rocket_launch_24,
resourceReference(R.string.user_push_notification_agreement_argument_one),
iconRes = R.drawable.ic_notification_square_24,
title = resourceReference(R.string.user_push_notification_agreement_argument_one_title),
subTitle = resourceReference(R.string.user_push_notification_agreement_argument_one_subtitle),
),
ShowcaseItemModel(
R.drawable.ic_storefront_24,
resourceReference(R.string.user_push_notification_agreement_argument_two),
),
ShowcaseItemModel(
R.drawable.ic_notifications_24,
resourceReference(R.string.user_push_notification_agreement_argument_three),
iconRes = R.drawable.ic_stars_24,
title = resourceReference(R.string.user_push_notification_agreement_argument_two_title),
subTitle = resourceReference(R.string.user_push_notification_agreement_argument_two_subtitle),
),
),
modifier = Modifier.padding(top = TangemTheme.dimens.spacing40),

View file

@ -26,20 +26,18 @@ internal fun PushNotificationsScreen(
)
Showcase(
headerIconRes = R.drawable.ic_notifications_unread_24,
headerIconRes = R.drawable.ic_notification_56,
headerText = resourceReference(R.string.user_push_notification_agreement_header),
showcaseItems = persistentListOf(
ShowcaseItemModel(
R.drawable.ic_rocket_launch_24,
resourceReference(R.string.user_push_notification_agreement_argument_one),
iconRes = R.drawable.ic_notification_square_24,
title = resourceReference(R.string.user_push_notification_agreement_argument_one_title),
subTitle = resourceReference(R.string.user_push_notification_agreement_argument_one_subtitle),
),
ShowcaseItemModel(
R.drawable.ic_storefront_24,
resourceReference(R.string.user_push_notification_agreement_argument_two),
),
ShowcaseItemModel(
R.drawable.ic_notifications_24,
resourceReference(R.string.user_push_notification_agreement_argument_three),
iconRes = R.drawable.ic_stars_24,
title = resourceReference(R.string.user_push_notification_agreement_argument_two_title),
subTitle = resourceReference(R.string.user_push_notification_agreement_argument_two_subtitle),
),
),
primaryButton = ShowcaseButtonModel(

View file

@ -18,22 +18,50 @@ sealed class CommonSendAnalyticEvents(
/** Recipient address screen opened */
data class AddressScreenOpened(
val categoryName: String,
) : CommonSendAnalyticEvents(category = categoryName, event = "Address Screen Opened")
val source: CommonSendSource,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Address Screen Opened",
params = mapOf(
SOURCE to source.analyticsName,
),
)
/** Amount screen opened */
data class AmountScreenOpened(
val categoryName: String,
) : CommonSendAnalyticEvents(category = categoryName, event = "Amount Screen Opened")
val source: CommonSendSource,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Amount Screen Opened",
params = mapOf(
SOURCE to source.analyticsName,
),
)
/** Fee screen opened */
data class FeeScreenOpened(
val categoryName: String,
) : CommonSendAnalyticEvents(category = categoryName, event = "Fee Screen Opened")
val source: CommonSendSource,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Fee Screen Opened",
params = mapOf(
SOURCE to source.analyticsName,
),
)
/** Confirmation screen opened */
data class ConfirmationScreenOpened(
val categoryName: String,
) : CommonSendAnalyticEvents(category = categoryName, event = "Confirm Screen Opened")
val source: CommonSendSource,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Confirm Screen Opened",
params = mapOf(
SOURCE to source.analyticsName,
),
)
/** If transaction delays notification is present */
data class NoticeTransactionDelays(
@ -142,4 +170,11 @@ sealed class CommonSendAnalyticEvents(
Fee,
Confirm,
}
enum class CommonSendSource(val analyticsName: String) {
Send("Send"),
SendWithSwap("Send&Swap"),
WalletConnect("WalletConnect"),
NFT("NFT"),
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
@ -17,6 +18,7 @@ sealed class FeeSelectorParams {
abstract val feeStateConfiguration: FeeStateConfiguration
abstract val feeDisplaySource: FeeDisplaySource
abstract val analyticsCategoryName: String
abstract val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource
data class FeeSelectorBlockParams(
override val state: FeeSelectorUM,
@ -26,6 +28,7 @@ sealed class FeeSelectorParams {
override val feeStateConfiguration: FeeStateConfiguration,
override val feeDisplaySource: FeeDisplaySource,
override val analyticsCategoryName: String,
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
) : FeeSelectorParams()
data class FeeSelectorDetailsParams(
@ -36,6 +39,7 @@ sealed class FeeSelectorParams {
override val feeStateConfiguration: FeeStateConfiguration,
override val feeDisplaySource: FeeDisplaySource,
override val analyticsCategoryName: String,
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
val callback: FeeSelectorModelCallback,
) : FeeSelectorParams()

View file

@ -2,8 +2,10 @@ package com.tangem.features.send.v2.api.subcomponents.amount.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
sealed class CommonSendAmountAnalyticEvents(
category: String,
@ -26,12 +28,14 @@ sealed class CommonSendAmountAnalyticEvents(
val categoryName: String,
val token: String,
val blockchain: String,
val source: CommonSendAnalyticEvents.CommonSendSource,
) : CommonSendAmountAnalyticEvents(
category = categoryName,
event = "Max Amount Taped",
params = mapOf(
TOKEN_PARAM to token,
BLOCKCHAIN to blockchain,
SOURCE to source.analyticsName,
),
)

View file

@ -3,6 +3,7 @@ package com.tangem.features.send.v2.api.subcomponents.destination
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.entity.PredefinedValues
import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM
import kotlinx.coroutines.flow.Flow
@ -12,12 +13,14 @@ sealed class SendDestinationComponentParams {
abstract val state: DestinationUM
abstract val analyticsCategoryName: String
abstract val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource
abstract val userWalletId: UserWalletId
abstract val cryptoCurrency: CryptoCurrency
data class DestinationParams(
override val state: DestinationUM,
override val analyticsCategoryName: String,
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
override val cryptoCurrency: CryptoCurrency,
override val userWalletId: UserWalletId,
val title: TextReference,
@ -29,6 +32,7 @@ sealed class SendDestinationComponentParams {
data class DestinationBlockParams(
override val state: DestinationUM,
override val analyticsCategoryName: String,
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
override val userWalletId: UserWalletId,
override val cryptoCurrency: CryptoCurrency,
val blockClickEnableFlow: StateFlow<Boolean>,

View file

@ -2,6 +2,9 @@ package com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.CommonSendSource
sealed class CommonSendFeeAnalyticEvents(
category: String,
@ -15,10 +18,14 @@ sealed class CommonSendFeeAnalyticEvents(
data class SelectedFee(
override val categoryName: String,
val feeType: AnalyticsParam.FeeType,
val source: CommonSendSource,
) : CommonSendFeeAnalyticEvents(
category = categoryName,
event = "Fee Selected",
params = mapOf("Fee Type" to feeType.value),
params = mapOf(
FEE_TYPE to feeType.value,
SOURCE to source.analyticsName,
),
)
/** Custom fee selected */

View file

@ -51,6 +51,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
feeStateConfiguration = params.feeStateConfiguration,
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
analyticsCategoryName = params.analyticsCategoryName,
analyticsSendSource = params.analyticsSendSource,
),
onDismiss = {
model.feeSelectorBottomSheet.dismiss()

View file

@ -18,16 +18,16 @@ internal class DefaultFeeSelectorReloadTrigger @Inject constructor() :
FeeSelectorCheckReloadListener {
override val reloadTriggerFlow: SharedFlow<FeeSelectorData>
field = MutableSharedFlow()
field = MutableSharedFlow()
override val loadingStateTriggerFlow: SharedFlow<Unit>
field = MutableSharedFlow()
field = MutableSharedFlow()
override val checkReloadTriggerFlow: SharedFlow<Unit>
field = MutableSharedFlow()
field = MutableSharedFlow()
override val checkReloadResultFlow: SharedFlow<Boolean>
field = MutableSharedFlow()
field = MutableSharedFlow()
override suspend fun triggerUpdate(feeData: FeeSelectorData) {
reloadTriggerFlow.emit(feeData)

View file

@ -29,6 +29,7 @@ import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorRelo
import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents
import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents.GasPriceInserter
import com.tangem.features.send.v2.feeselector.model.transformers.*
import com.tangem.utils.TangemLinks
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.transformer.update
import kotlinx.coroutines.flow.MutableStateFlow
@ -36,7 +37,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.util.Locale
import javax.inject.Inject
@Suppress("LongParameterList")
@ -62,7 +62,7 @@ internal class FeeSelectorModel @Inject constructor(
val feeSelectorBottomSheet = SlotNavigation<Unit>()
val uiState: StateFlow<FeeSelectorUM>
field = MutableStateFlow<FeeSelectorUM>(params.state)
field = MutableStateFlow<FeeSelectorUM>(params.state)
init {
initAppCurrency()
@ -77,13 +77,7 @@ internal class FeeSelectorModel @Inject constructor(
}
fun onReadMoreClicked() {
val locale = if (Locale.getDefault().language == RU_LOCALE) RU_LOCALE else EN_LOCALE
val url = buildString {
append(FEE_READ_MORE_URL_FIRST_PART)
append(locale)
append(FEE_READ_MORE_URL_SECOND_PART)
}
urlOpener.openUrl(url)
urlOpener.openUrl(TangemLinks.FEE_BLOG_LINK)
}
private fun initAppCurrency() {
@ -156,6 +150,7 @@ internal class FeeSelectorModel @Inject constructor(
CommonSendFeeAnalyticEvents.SelectedFee(
categoryName = params.analyticsCategoryName,
feeType = feeSelectorUM.toAnalyticType(),
source = params.analyticsSendSource,
),
)
val isCustomFeeEdited = feeSelectorUM.selectedFeeItem.fee.amount.value != feeSelectorUM.fees.normal.amount.value
@ -182,7 +177,10 @@ internal class FeeSelectorModel @Inject constructor(
fun showFeeSelector() {
analyticsEventHandler.send(
CommonSendAnalyticEvents.FeeScreenOpened(categoryName = params.analyticsCategoryName),
CommonSendAnalyticEvents.FeeScreenOpened(
categoryName = params.analyticsCategoryName,
source = params.analyticsSendSource,
),
)
analyticsEventHandler.send(
CommonSendAnalyticEvents.ScreenReopened(
@ -245,11 +243,4 @@ internal class FeeSelectorModel @Inject constructor(
)
}
}
private companion object {
const val RU_LOCALE = "ru"
const val EN_LOCALE = "en"
const val FEE_READ_MORE_URL_FIRST_PART = "https://tangem.com/"
const val FEE_READ_MORE_URL_SECOND_PART = "/blog/post/what-is-a-transaction-fee-and-why-do-we-need-it/"
}
}

View file

@ -93,6 +93,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
analyticsEventHandler.send(
CommonSendAnalyticEvents.ConfirmationScreenOpened(
categoryName = model.analyticCategoryName,
source = model.analyticsSendSource,
),
)
if (model.currentRoute.value.isEditMode) {
@ -101,19 +102,28 @@ internal class DefaultSendComponent @AssistedInject constructor(
}
is SendAmountComponent -> {
analyticsEventHandler.send(
CommonSendAnalyticEvents.AmountScreenOpened(categoryName = model.analyticCategoryName),
CommonSendAnalyticEvents.AmountScreenOpened(
categoryName = model.analyticCategoryName,
source = model.analyticsSendSource,
),
)
activeComponent.updateState(model.uiState.value.amountUM)
}
is DefaultSendDestinationComponent -> {
analyticsEventHandler.send(
CommonSendAnalyticEvents.AddressScreenOpened(categoryName = model.analyticCategoryName),
CommonSendAnalyticEvents.AddressScreenOpened(
categoryName = model.analyticCategoryName,
source = model.analyticsSendSource,
),
)
activeComponent.updateState(model.uiState.value.destinationUM)
}
is SendFeeComponent -> {
analyticsEventHandler.send(
CommonSendAnalyticEvents.FeeScreenOpened(categoryName = model.analyticCategoryName),
CommonSendAnalyticEvents.FeeScreenOpened(
categoryName = model.analyticCategoryName,
source = model.analyticsSendSource,
),
)
}
}
@ -155,6 +165,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
currentRoute = model.currentRoute.filterIsInstance<CommonSendRoute.Destination>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = model.analyticCategoryName,
analyticsSendSource = model.analyticsSendSource,
title = resourceReference(R.string.common_address),
userWalletId = params.userWalletId,
cryptoCurrency = params.currency,
@ -177,6 +188,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
callback = model,
predefinedValues = model.predefinedValues,
isRedesignEnabled = model.uiState.value.isRedesignEnabled,
analyticsSendSource = model.analyticsSendSource,
),
)
}
@ -204,6 +216,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
sendAmount = sendAmount,
destinationAddress = destinationAddress,
callback = model,
analyticsSendSource = model.analyticsSendSource,
),
)
} else {
@ -238,6 +251,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
onSendTransaction = {
innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess)
},
analyticsSendSource = model.analyticsSendSource,
),
feeSelectorComponentFactory = feeSelectorComponentFactory,
)
@ -268,6 +282,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
params = SendDestinationComponentParams.DestinationBlockParams(
state = model.uiState.value.destinationUM,
analyticsCategoryName = model.analyticCategoryName,
analyticsSendSource = model.analyticsSendSource,
userWalletId = model.userWallet.walletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
blockClickEnableFlow = MutableStateFlow(true),

View file

@ -17,6 +17,7 @@ import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
import com.tangem.features.send.v2.api.SendNotificationsComponent
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.entity.PredefinedValues
import com.tangem.features.send.v2.api.params.FeeSelectorParams
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
@ -50,6 +51,7 @@ internal class SendConfirmComponent(
params = DestinationBlockParams(
state = model.uiState.value.destinationUM,
analyticsCategoryName = params.analyticsCategoryName,
analyticsSendSource = params.analyticsSendSource,
userWalletId = params.userWallet.walletId,
cryptoCurrency = params.cryptoCurrencyStatus.currency,
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
@ -73,6 +75,7 @@ internal class SendConfirmComponent(
cryptoCurrency = params.cryptoCurrencyStatus.currency,
cryptoCurrencyStatusFlow = params.cryptoCurrencyStatusFlow,
isBalanceHidingFlow = params.isBalanceHidingFlow,
analyticsSendSource = params.analyticsSendSource,
),
onResult = model::onAmountResult,
onClick = model::showEditAmount,
@ -90,6 +93,7 @@ internal class SendConfirmComponent(
sendAmount = model.confirmData.enteredAmount.orZero(),
destinationAddress = model.confirmData.enteredDestination.orEmpty(),
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
analyticsSendSource = params.analyticsSendSource,
onLoadFee = params.onLoadFee,
),
onResult = model::onFeeResult,
@ -106,6 +110,7 @@ internal class SendConfirmComponent(
feeStateConfiguration = model.feeStateConfiguration,
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
analyticsCategoryName = params.analyticsCategoryName,
analyticsSendSource = params.analyticsSendSource,
),
onResult = model::onFeeResult,
)
@ -165,6 +170,7 @@ internal class SendConfirmComponent(
data class Params(
val state: SendUM,
val analyticsCategoryName: String,
val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
val userWallet: UserWallet,
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,

View file

@ -127,7 +127,7 @@ internal class SendConfirmModel @Inject constructor(
val uiState = _uiState.asStateFlow()
val isBalanceHiddenFlow: StateFlow<Boolean>
field = MutableStateFlow(false)
field = MutableStateFlow(false)
private val amountState
get() = uiState.value.amountUM as? AmountState.Data
@ -317,7 +317,7 @@ internal class SendConfirmModel @Inject constructor(
null
}
val amount = receivingAmount?.convertToSdkAmount(cryptoCurrency)
val amount = receivingAmount?.convertToSdkAmount(cryptoCurrencyStatus)
saveBlockchainErrorUseCase(
error = BlockchainErrorInfo(
@ -331,7 +331,7 @@ internal class SendConfirmModel @Inject constructor(
""
},
amount = amount?.value?.stripZeroPlainString() ?: "unknown",
fee = feeValue?.convertToSdkAmount(cryptoCurrency)
fee = feeValue?.convertToSdkAmount(cryptoCurrencyStatus)
?.value?.stripZeroPlainString() ?: "unknown",
),
)
@ -424,7 +424,7 @@ internal class SendConfirmModel @Inject constructor(
modelScope.launch {
createTransferTransactionUseCase(
amount = receivingAmount.convertToSdkAmount(cryptoCurrency),
amount = receivingAmount.convertToSdkAmount(cryptoCurrencyStatus),
fee = fee,
memo = memo,
nonce = nonce,

View file

@ -104,12 +104,13 @@ internal class SendModel @Inject constructor(
private val cryptoCurrency = params.currency
val analyticCategoryName = CommonSendAnalyticEvents.SEND_CATEGORY
val analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Send
val uiState: StateFlow<SendUM>
field = MutableStateFlow(initialState())
field = MutableStateFlow(initialState())
val isBalanceHiddenFlow: StateFlow<Boolean>
field = MutableStateFlow(false)
field = MutableStateFlow(false)
val initialRoute = if (params.amount == null) {
if (uiState.value.isRedesignEnabled) {
@ -260,10 +261,11 @@ internal class SendModel @Inject constructor(
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
val predefinedValues = predefinedValues
val cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value
val transferTransaction = if (predefinedValues is PredefinedValues.Content.Deeplink) {
val predefinedAmount = predefinedValues.amount.parseBigDecimalOrNull()?.convertToSdkAmount(cryptoCurrency)
val predefinedAmount = predefinedValues.amount.parseBigDecimalOrNull()
createTransferTransactionUseCase(
amount = predefinedAmount ?: error("Invalid amount"),
amount = predefinedAmount?.convertToSdkAmount(cryptoCurrencyStatus) ?: error("Invalid amount"),
memo = predefinedValues.memo,
destination = predefinedValues.address,
userWalletId = userWallet.walletId,
@ -277,7 +279,7 @@ internal class SendModel @Inject constructor(
val enteredAmount = amountUM.amountTextField.cryptoAmount.value ?: error("Invalid amount")
createTransferTransactionUseCase(
amount = enteredAmount.convertToSdkAmount(cryptoCurrency),
amount = enteredAmount.convertToSdkAmount(cryptoCurrencyStatus),
memo = enteredMemo,
destination = enteredDestinationAddress,
userWalletId = userWallet.walletId,

View file

@ -81,10 +81,10 @@ internal class SendConfirmSuccessModel @Inject constructor(
iconRes = R.drawable.ic_web_24,
onClick = ::onExploreClick,
) to NavigationButton(
textReference = resourceReference(R.string.common_share),
iconRes = R.drawable.ic_share_24,
onClick = ::onShareClick,
)).takeIf { params.txUrl.isNotEmpty() },
textReference = resourceReference(R.string.common_share),
iconRes = R.drawable.ic_share_24,
onClick = ::onShareClick,
)).takeIf { params.txUrl.isNotEmpty() },
),
),
)

View file

@ -49,6 +49,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
private val stackNavigation = StackNavigation<CommonSendRoute>()
private val analyticsCategoryName = CommonSendAnalyticEvents.NFT_SEND_CATEGORY
private val analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.NFT
private val innerRouter = InnerRouter<CommonSendRoute>(
stackNavigation = stackNavigation,
@ -83,7 +84,10 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
when (val activeComponent = stack.active.instance) {
is NFTSendConfirmComponent -> {
analyticsEventHandler.send(
CommonSendAnalyticEvents.ConfirmationScreenOpened(categoryName = analyticsCategoryName),
CommonSendAnalyticEvents.ConfirmationScreenOpened(
categoryName = analyticsCategoryName,
source = analyticsSendSource,
),
)
if (model.currentRouteFlow.value.isEditMode) {
activeComponent.updateState(model.uiState.value)
@ -91,13 +95,19 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
}
is DefaultSendDestinationComponent -> {
analyticsEventHandler.send(
CommonSendAnalyticEvents.AddressScreenOpened(categoryName = analyticsCategoryName),
CommonSendAnalyticEvents.AddressScreenOpened(
categoryName = analyticsCategoryName,
source = analyticsSendSource,
),
)
activeComponent.updateState(model.uiState.value.destinationUM)
}
is SendFeeComponent -> {
analyticsEventHandler.send(
CommonSendAnalyticEvents.FeeScreenOpened(categoryName = analyticsCategoryName),
CommonSendAnalyticEvents.FeeScreenOpened(
categoryName = analyticsCategoryName,
source = analyticsSendSource,
),
)
}
}
@ -135,6 +145,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
isBalanceHidingFlow = model.isBalanceHiddenFlow,
title = resourceReference(R.string.nft_send),
analyticsCategoryName = analyticsCategoryName,
analyticsSendSource = analyticsSendSource,
userWalletId = params.userWalletId,
cryptoCurrency = model.cryptoCurrency,
callback = model,
@ -159,6 +170,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
onLoadFee = model::loadFee,
destinationAddress = destinationAddress,
callback = model,
analyticsSendSource = analyticsSendSource,
),
)
} else {
@ -181,6 +193,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
currentRoute = model.currentRouteFlow.filterIsInstance<CommonSendRoute.Confirm>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
onLoadFee = model::loadFee,
analyticsSendSource = analyticsSendSource,
onSendTransaction = { innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess) },
),
)
@ -198,6 +211,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
params = NFTSendSuccessComponent.Params(
nftSendUMFlow = model.uiState,
analyticsCategoryName = analyticsCategoryName,
analyticsSendSource = analyticsSendSource,
userWallet = model.userWallet,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
nftAsset = params.nftAsset,

View file

@ -20,6 +20,7 @@ import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.features.nft.component.NFTDetailsBlockComponent
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
import com.tangem.features.send.v2.api.SendNotificationsComponent
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.entity.PredefinedValues
import com.tangem.features.send.v2.api.params.FeeSelectorParams
import com.tangem.features.send.v2.api.params.FeeSelectorParams.FeeStateConfiguration
@ -57,6 +58,7 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
params = DestinationBlockParams(
state = model.uiState.value.destinationUM,
analyticsCategoryName = params.analyticsCategoryName,
analyticsSendSource = params.analyticsSendSource,
userWalletId = params.userWallet.walletId,
cryptoCurrency = params.cryptoCurrencyStatus.currency,
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
@ -79,6 +81,7 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
onLoadFee = params.onLoadFee,
destinationAddress = model.confirmData.enteredDestination.orEmpty(),
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
analyticsSendSource = params.analyticsSendSource,
),
onResult = model::onFeeResult,
onClick = model::showEditFee,
@ -94,6 +97,7 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
feeStateConfiguration = FeeStateConfiguration.None,
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
analyticsCategoryName = params.analyticsCategoryName,
analyticsSendSource = params.analyticsSendSource,
),
onResult = model::onFeeResult,
)
@ -162,6 +166,7 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
data class Params(
val state: NFTSendUM,
val analyticsCategoryName: String,
val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
val userWallet: UserWallet,
val appCurrency: AppCurrency,
val nftAsset: NFTAsset,

View file

@ -88,10 +88,10 @@ internal class NFTSendModel @Inject constructor(
private val nftAsset = params.nftAsset
val uiState: StateFlow<NFTSendUM>
field = MutableStateFlow(initialState())
field = MutableStateFlow(initialState())
val isBalanceHiddenFlow: StateFlow<Boolean>
field = MutableStateFlow(false)
field = MutableStateFlow(false)
var cryptoCurrency: CryptoCurrency by Delegates.notNull()
var userWallet: UserWallet by Delegates.notNull()

View file

@ -14,6 +14,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.features.nft.component.NFTDetailsBlockComponent
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.entity.PredefinedValues
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent
import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
@ -54,6 +55,7 @@ internal class NFTSendSuccessComponent @AssistedInject constructor(
params = DestinationBlockParams(
state = model.uiState.value.destinationUM,
analyticsCategoryName = params.analyticsCategoryName,
analyticsSendSource = params.analyticsSendSource,
userWalletId = params.userWallet.walletId,
cryptoCurrency = params.cryptoCurrencyStatus.currency,
blockClickEnableFlow = MutableStateFlow(false),
@ -77,6 +79,7 @@ internal class NFTSendSuccessComponent @AssistedInject constructor(
data class Params(
val nftSendUMFlow: StateFlow<NFTSendUM>,
val analyticsCategoryName: String,
val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
val currentRoute: Flow<CommonSendRoute>,
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val userWallet: UserWallet,

View file

@ -6,6 +6,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.api.entity.PredefinedValues
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent.ModelCallback
@ -15,6 +16,7 @@ internal sealed class SendAmountComponentParams {
abstract val state: AmountState
abstract val analyticsCategoryName: String
abstract val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource
abstract val userWalletId: UserWalletId
abstract val appCurrency: AppCurrency
abstract val predefinedValues: PredefinedValues
@ -33,6 +35,7 @@ internal sealed class SendAmountComponentParams {
override val cryptoCurrency: CryptoCurrency,
override val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
override val isBalanceHidingFlow: StateFlow<Boolean>,
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
val callback: ModelCallback,
val currentRoute: StateFlow<CommonSendRoute>,
) : SendAmountComponentParams()
@ -47,6 +50,7 @@ internal sealed class SendAmountComponentParams {
override val cryptoCurrency: CryptoCurrency,
override val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
override val isBalanceHidingFlow: StateFlow<Boolean>,
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
val userWallet: UserWallet,
val blockClickEnableFlow: StateFlow<Boolean>,
) : SendAmountComponentParams()

View file

@ -79,7 +79,7 @@ internal class SendAmountModel @Inject constructor(
private var isAvailableForSwap: Boolean = false
val isSendWithSwapAvailable: StateFlow<Boolean>
field = MutableStateFlow(false)
field = MutableStateFlow(false)
private val analyticsCategoryName = params.analyticsCategoryName
private var cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
@ -246,6 +246,7 @@ internal class SendAmountModel @Inject constructor(
categoryName = analyticsCategoryName,
token = params.cryptoCurrency.symbol,
blockchain = params.cryptoCurrency.network.name,
source = params.analyticsSendSource,
),
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.send.v2.subcomponents.destination.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.VALIDATION
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
internal sealed class SendDestinationAnalyticEvents(
category: String,
@ -15,13 +16,15 @@ internal sealed class SendDestinationAnalyticEvents(
/** Address to send entered */
data class AddressEntered(
override val categoryName: String,
val source: EnterAddressSource,
val method: EnterAddressSource,
val isValid: Boolean,
val source: CommonSendAnalyticEvents.CommonSendSource,
) : SendDestinationAnalyticEvents(
category = categoryName,
event = "Address Entered",
params = mapOf(
SOURCE to source.name,
SOURCE to source.analyticsName,
"Method" to method.name,
VALIDATION to if (isValid) "Success" else "Fail",
),
)

View file

@ -275,7 +275,8 @@ internal class SendDestinationModel @Inject constructor(
analyticsEventHandler.send(
SendDestinationAnalyticEvents.AddressEntered(
categoryName = analyticsCategoryName,
source = it,
source = params.analyticsSendSource,
method = it,
isValid = addressValidationResult.isRight(),
),
)

View file

@ -6,6 +6,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import kotlinx.coroutines.flow.Flow
@ -23,6 +24,7 @@ internal sealed class SendFeeComponentParams {
abstract val sendAmount: BigDecimal
abstract val destinationAddress: String
abstract val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>
abstract val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource
data class FeeParams(
override val state: FeeUM,
@ -34,6 +36,7 @@ internal sealed class SendFeeComponentParams {
override val sendAmount: BigDecimal,
override val destinationAddress: String,
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
val currentRoute: Flow<CommonSendRoute.Fee>,
val callback: SendFeeComponent.ModelCallback,
) : SendFeeComponentParams()
@ -48,6 +51,7 @@ internal sealed class SendFeeComponentParams {
override val sendAmount: BigDecimal,
override val destinationAddress: String,
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
val blockClickEnableFlow: StateFlow<Boolean>,
) : SendFeeComponentParams()
}

View file

@ -22,13 +22,13 @@ import com.tangem.features.send.v2.subcomponents.fee.model.transformers.*
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.TangemLinks
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.transformer.update
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.util.Locale
import javax.inject.Inject
@Suppress("LongParameterList")
@ -136,13 +136,7 @@ internal class SendFeeModel @Inject constructor(
}
override fun onReadMoreClick() {
val locale = if (Locale.getDefault().language == RU_LOCALE) RU_LOCALE else EN_LOCALE
val url = buildString {
append(FEE_READ_MORE_URL_FIRST_PART)
append(locale)
append(FEE_READ_MORE_URL_SECOND_PART)
}
urlOpener.openUrl(url)
urlOpener.openUrl(TangemLinks.FEE_BLOG_LINK)
}
override fun onNextClick() {
@ -162,6 +156,7 @@ internal class SendFeeModel @Inject constructor(
CommonSendFeeAnalyticEvents.SelectedFee(
categoryName = analyticsCategoryName,
feeType = feeSelectorUM.selectedType.toAnalyticType(feeSelectorUM),
source = params.analyticsSendSource,
),
)
@ -297,11 +292,4 @@ internal class SendFeeModel @Inject constructor(
)
}.launchIn(modelScope)
}
private companion object {
const val RU_LOCALE = "ru"
const val EN_LOCALE = "en"
const val FEE_READ_MORE_URL_FIRST_PART = "https://tangem.com/"
const val FEE_READ_MORE_URL_SECOND_PART = "/blog/post/what-is-a-transaction-fee-and-why-do-we-need-it/"
}
}

View file

@ -287,7 +287,7 @@ internal class NotificationsModel @Inject constructor(
) {
val validationError = validateTransactionUseCase(
userWalletId = userWalletId,
amount = enteredAmount.convertToSdkAmount(currency),
amount = enteredAmount.convertToSdkAmount(cryptoCurrencyStatus),
fee = fee,
memo = memo,
destination = destinationAddress,

View file

@ -263,7 +263,7 @@ internal class StakingModel @Inject constructor(
isInitialInfoStep && noBalanceState && !isAccountInitialized -> {
analyticsEventHandler.send(StakingAnalyticsEvent.UnitializedAddress(
token = cryptoCurrencyStatus.currency.symbol,
),)
))
stakingEventFactory.createInitializeAccountAlert()
return@launch
}
@ -662,7 +662,7 @@ internal class StakingModel @Inject constructor(
contractAddress = tokenCryptoCurrency.contractAddress,
spenderAddress = approval.spenderAddress,
fee = fee,
cryptoCurrency = tokenCryptoCurrency,
cryptoCurrencyStatus = cryptoCurrencyStatus,
userWalletId = userWalletId,
).fold(
ifLeft = { error ->

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