Updated on 2026-08-14

This commit is contained in:
Tangem 2023-10-23 16:25:17 +08:00
parent e881eaa3de
commit 9ec1cc5ba8
11 changed files with 178 additions and 80 deletions

View file

@ -38,7 +38,7 @@ internal class DefaultCurrenciesRepository(
) : CurrenciesRepository {
private val demoConfig = DemoConfig()
private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(demoConfig)
private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory()
private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig)
private val userTokensResponseFactory = UserTokensResponseFactory()
private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility()
@ -315,6 +315,18 @@ internal class DefaultCurrenciesRepository(
private suspend fun fetchTokens(userWallet: UserWallet) {
val userWalletId = userWallet.walletId
if (demoConfig.isDemoCardId(userWallet.cardId) && userTokensStore.getSyncOrNull(key = userWalletId) == null) {
userTokensStore.store(
key = userWalletId,
value = userTokensResponseFactory.createUserTokensResponse(
currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse),
isGroupedByNetwork = false,
isSortedByBalance = false,
),
)
return
}
val response = safeApiCall(
call = {
tangemTechApi.getUserTokens(userWalletId.stringValue).bind().let {

View file

@ -35,7 +35,7 @@ internal class DefaultNetworksRepository(
private val demoConfig by lazy { DemoConfig() }
private val cardCurrenciesFactory by lazy { CardCryptoCurrenciesFactory(demoConfig) }
private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory(demoConfig) }
private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory() }
private val networkStatusFactory by lazy { NetworkStatusFactory() }
override fun getNetworkStatusesUpdates(

View file

@ -4,17 +4,17 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import timber.log.Timber
import com.tangem.blockchain.common.Token as SdkToken
internal class ResponseCryptoCurrenciesFactory(private val demoConfig: DemoConfig) {
internal class ResponseCryptoCurrenciesFactory {
fun createCurrency(
currencyId: CryptoCurrency.ID,
@ -54,7 +54,7 @@ internal class ResponseCryptoCurrenciesFactory(private val demoConfig: DemoConfi
val cardDerivationStyleProvider = scanResponse.derivationStyleProvider
val card = scanResponse.card
if (demoConfig.isDemoCardId(card.cardId)) {
if (card.isTestCard) {
blockchain = blockchain.getTestnetVersion() ?: blockchain
}

View file

@ -52,6 +52,7 @@ dependencies {
/** Domain modules */
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.demo)
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.settings)

View file

@ -3,13 +3,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig
import com.tangem.features.tokendetails.impl.R
@ -108,5 +105,6 @@ internal object TokenDetailsPreviewData {
bottomSheetConfig = null,
isBalanceHidden = false,
isMarketPriceAvailable = false,
event = consumedEvent(),
)
}

View file

@ -4,6 +4,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig
@ -23,4 +25,5 @@ internal data class TokenDetailsState(
val bottomSheetConfig: TangemBottomSheetConfig?,
val isBalanceHidden: Boolean,
val isMarketPriceAvailable: Boolean,
val event: StateEvent<TextReference>,
)

View file

@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.res.TangemTheme
@ -54,6 +55,7 @@ internal class TokenDetailsSkeletonStateConverter(
bottomSheetConfig = null,
isBalanceHidden = true,
isMarketPriceAvailable = value.id.rawCurrencyId != null,
event = consumedEvent(),
)
}

View file

@ -9,6 +9,9 @@ import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBot
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrency
@ -225,4 +228,20 @@ internal class TokenDetailsStateFactory(
val state = currentStateProvider()
return state.copy(notifications = notificationConverter.removeRentInfo(state))
}
fun getStateAndTriggerEvent(
state: TokenDetailsState,
errorMessage: TextReference,
setUiState: (TokenDetailsState) -> Unit,
): TokenDetailsState {
return state.copy(
event = triggeredEvent(
data = errorMessage,
onConsume = {
val currentState = currentStateProvider()
setUiState(currentState.copy(event = consumedEvent()))
},
),
)
}
}

View file

@ -11,10 +11,14 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEach
import androidx.paging.compose.collectAsLazyPagingItems
@ -29,6 +33,10 @@ import com.tangem.core.ui.components.transactions.Transaction
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.components.transactions.txHistoryItems
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
@ -46,8 +54,10 @@ import kotlinx.collections.immutable.PersistentList
internal fun TokenDetailsScreen(state: TokenDetailsState) {
BackHandler(onBack = state.topAppBarConfig.onBackClick)
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
containerColor = TangemTheme.colors.background.secondary,
) { scaffoldPaddings ->
val pullRefreshState = rememberPullRefreshState(
@ -138,17 +148,17 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
TokenDetailsDialogs(state = state)
state.bottomSheetConfig?.let { config ->
if (config.isShow) {
when (config.content) {
is TokenReceiveBottomSheetConfig -> {
TokenReceiveBottomSheet(config = config)
}
is ChooseAddressBottomSheetConfig -> {
ChooseAddressBottomSheet(config = config)
}
when (config.content) {
is TokenReceiveBottomSheetConfig -> {
TokenReceiveBottomSheet(config = config)
}
is ChooseAddressBottomSheetConfig -> {
ChooseAddressBottomSheet(config = config)
}
}
}
TokenDetailsEventEffect(snackbarHostState = snackbarHostState, event = state.event)
}
}
@ -169,6 +179,18 @@ private fun PendingTxsBlock(
}
}
@Composable
internal fun TokenDetailsEventEffect(snackbarHostState: SnackbarHostState, event: StateEvent<TextReference>) {
val resources = LocalContext.current.resources
EventEffect(
event = event,
onTrigger = { value ->
snackbarHostState.showSnackbar(message = value.resolveReference(resources))
},
)
}
@Preview
@Composable
private fun Preview_TokenDetailsScreen_LightTheme() {

View file

@ -11,10 +11,12 @@ import com.tangem.common.Provider
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
@ -33,6 +35,7 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.features.tokendetails.impl.R
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
@ -65,6 +68,7 @@ internal class TokenDetailsViewModel @Inject constructor(
private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val walletManagersFacade: WalletManagersFacade,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val reduxStateHolder: ReduxStateHolder,
private val analyticsEventsHandler: AnalyticsEventHandler,
savedStateHandle: SavedStateHandle,
@ -225,16 +229,19 @@ internal class TokenDetailsViewModel @Inject constructor(
override fun onBuyClick() {
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonBuy(cryptoCurrency.symbol))
val status = cryptoCurrencyStatus ?: return
viewModelScope.launch(dispatchers.io) {
reduxStateHolder.dispatch(
TradeCryptoAction.New.Buy(
userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch },
cryptoCurrencyStatus = status,
appCurrencyCode = selectedAppCurrencyFlow.value.code,
),
)
showErrorIfDemoModeOrElse {
val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse
viewModelScope.launch(dispatchers.io) {
reduxStateHolder.dispatch(
TradeCryptoAction.New.Buy(
userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch },
cryptoCurrencyStatus = status,
appCurrencyCode = selectedAppCurrencyFlow.value.code,
),
)
}
}
}
@ -313,13 +320,16 @@ internal class TokenDetailsViewModel @Inject constructor(
override fun onSellClick() {
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonSell(cryptoCurrency.symbol))
val status = cryptoCurrencyStatus ?: return
reduxStateHolder.dispatch(
TradeCryptoAction.New.Sell(
cryptoCurrencyStatus = status,
appCurrencyCode = selectedAppCurrencyFlow.value.code,
),
)
showErrorIfDemoModeOrElse {
val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse
reduxStateHolder.dispatch(
TradeCryptoAction.New.Sell(
cryptoCurrencyStatus = status,
appCurrencyCode = selectedAppCurrencyFlow.value.code,
),
)
}
}
override fun onSwapClick() {
@ -355,6 +365,10 @@ internal class TokenDetailsViewModel @Inject constructor(
override fun onExploreClick() {
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonExplore(cryptoCurrency.symbol))
showErrorIfDemoModeOrElse(action = ::openExplorer)
}
private fun openExplorer() {
viewModelScope.launch(dispatchers.io) {
val addresses = walletManagersFacade.getAddress(
userWalletId = userWalletId,
@ -375,6 +389,23 @@ internal class TokenDetailsViewModel @Inject constructor(
}
}
private fun showErrorIfDemoModeOrElse(action: () -> Unit) {
viewModelScope.launch(dispatchers.main) {
val wallet = getUserWalletUseCase(userWalletId = userWalletId).getOrElse { return@launch }
if (isDemoCardUseCase(cardId = wallet.cardId)) {
uiState = stateFactory.getStateWithClosedBottomSheet()
uiState = stateFactory.getStateAndTriggerEvent(
state = uiState,
errorMessage = resourceReference(id = R.string.alert_demo_feature_disabled),
setUiState = { uiState = it },
)
} else {
action()
}
}
}
override fun onAddressTypeSelected(addressModel: AddressModel) {
viewModelScope.launch {
router.openUrl(

View file

@ -130,9 +130,9 @@ internal class WalletViewModel @Inject constructor(
private val neverToSuggestRateAppUseCase: NeverToSuggestRateAppUseCase,
private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
wasCardScannedUseCase: WasCardScannedUseCase,
isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
isDemoCardUseCase: IsDemoCardUseCase,
isNeedToBackupUseCase: IsNeedToBackupUseCase,
getMissedAddressesCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase,
// endregion Parameters
@ -608,15 +608,15 @@ internal class WalletViewModel @Inject constructor(
event = TokenScreenAnalyticsEvent.ButtonBuy(cryptoCurrencyStatus.currency.symbol),
)
val wallet = getWallet(index = state.walletsListConfig.selectedWalletIndex)
reduxStateHolder.dispatch(
TradeCryptoAction.New.Buy(
userWallet = wallet,
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrencyCode = selectedAppCurrencyFlow.value.code,
),
)
showErrorIfDemoModeOrElse {
reduxStateHolder.dispatch(
TradeCryptoAction.New.Buy(
userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex),
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrencyCode = selectedAppCurrencyFlow.value.code,
),
)
}
}
override fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
@ -755,12 +755,14 @@ internal class WalletViewModel @Inject constructor(
event = TokenScreenAnalyticsEvent.ButtonSell(cryptoCurrencyStatus.currency.symbol),
)
reduxStateHolder.dispatch(
TradeCryptoAction.New.Sell(
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrencyCode = selectedAppCurrencyFlow.value.code,
),
)
showErrorIfDemoModeOrElse {
reduxStateHolder.dispatch(
action = TradeCryptoAction.New.Sell(
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrencyCode = selectedAppCurrencyFlow.value.code,
),
)
}
}
override fun onManageTokensClick() {
@ -803,22 +805,39 @@ internal class WalletViewModel @Inject constructor(
}
override fun onExploreClick() {
viewModelScope.launch(dispatchers.io) {
val wallet = getWallet(
index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex,
)
val currencyStatus = getPrimaryCurrencyStatusUpdatesUseCase(wallet.walletId)
.firstOrNull()
?.getOrNull()
showErrorIfDemoModeOrElse(action = ::openExplorer)
}
if (currencyStatus != null) {
router.openUrl(
url = getExploreUrlUseCase(
userWalletId = wallet.walletId,
network = currencyStatus.currency.network,
),
)
}
private fun openExplorer() {
val state = uiState as? WalletState.ContentState ?: return
viewModelScope.launch(dispatchers.main) {
val currencyStatus = singleWalletCryptoCurrencyStatus ?: return@launch
router.openUrl(
url = getExploreUrlUseCase(
userWalletId = getWallet(index = state.walletsListConfig.selectedWalletIndex).walletId,
network = currencyStatus.currency.network,
),
)
}
}
private fun showErrorIfDemoModeOrElse(action: () -> Unit) {
val state = uiState as? WalletState.ContentState ?: return
val cardId = getWallet(index = state.walletsListConfig.selectedWalletIndex).cardId
if (isDemoCardUseCase(cardId = cardId)) {
uiState = stateFactory.getStateWithClosedBottomSheet()
uiState = stateFactory.getStateAndTriggerEvent(
state = uiState,
event = WalletEvent.ShowError(
text = resourceReference(id = R.string.alert_demo_feature_disabled),
),
setUiState = { uiState = it },
)
} else {
action()
}
}
@ -934,22 +953,13 @@ internal class WalletViewModel @Inject constructor(
}
override fun onTransactionClick(txHash: String) {
viewModelScope.launch(dispatchers.io) {
val wallet = getWallet(
index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex,
singleWalletCryptoCurrencyStatus?.let { currencyStatus ->
router.openUrl(
url = getExplorerTransactionUrlUseCase(
txHash = txHash,
networkId = currencyStatus.currency.network.id,
),
)
val currencyStatus = getPrimaryCurrencyStatusUpdatesUseCase(wallet.walletId)
.firstOrNull()
?.getOrNull()
if (currencyStatus != null) {
router.openUrl(
url = getExplorerTransactionUrlUseCase(
txHash = txHash,
networkId = currencyStatus.currency.network.id,
),
)
}
}
}