Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-04 14:35:13 +03:00
commit 1d6b36877b
72 changed files with 1130 additions and 246 deletions

View file

@ -74,6 +74,7 @@ dependencies {
implementation(projects.data.tokens)
implementation(projects.data.txhistory)
implementation(projects.data.wallets)
implementation(projects.data.analytics)
/** Features */
implementation(projects.features.onboarding)

View file

@ -14,6 +14,7 @@ import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.api.common.MoshiConverter
@ -216,6 +217,9 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var sendFeatureToggles: SendFeatureToggles
@Inject
lateinit var oneTimeEventFilter: OneTimeEventFilter
// endregion Injected
override fun onCreate() {
@ -353,6 +357,8 @@ internal class TapApplication : Application(), ImageLoaderFactory {
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder())
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
factory.addFilter(oneTimeEventFilter)
val buildData = AnalyticsHandlerBuilder.Data(
application = application,
config = config,

View file

@ -9,8 +9,8 @@ class AmplitudeAnalyticsHandler(
override fun id(): String = ID
override fun send(event: String, params: Map<String, String>) {
client.logEvent(event, params)
override fun send(eventId: String, params: Map<String, String>) {
client.logEvent(eventId, params)
}
companion object {

View file

@ -12,8 +12,8 @@ class AppsFlyerAnalyticsHandler(
override fun id(): String = ID
override fun send(event: String, params: Map<String, String>) {
client.logEvent(event, params)
override fun send(eventId: String, params: Map<String, String>) {
client.logEvent(eventId, params)
}
override fun send(event: AnalyticsEvent) {

View file

@ -14,8 +14,8 @@ class FirebaseAnalyticsHandler(
override fun id(): String = ID
override fun send(event: String, params: Map<String, String>) {
client.logEvent(event, params)
override fun send(eventId: String, params: Map<String, String>) {
client.logEvent(eventId, params)
}
override fun send(event: AnalyticsEvent) {

View file

@ -2,10 +2,7 @@ package com.tangem.tap.di.domain
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -231,4 +228,10 @@ internal object TokensDomainModule {
): GetMissedAddressesCryptoCurrenciesUseCase {
return GetMissedAddressesCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository)
}
@Provides
@ViewModelScoped
fun provideGetGlobalTokenListUseCase(tokensListRepository: TokensListRepository): GetGlobalTokenListUseCase {
return GetGlobalTokenListUseCase(repository = tokensListRepository)
}
}

View file

@ -67,15 +67,17 @@ internal class BiometricUserWalletsListManager(
}
}
.map {
if (throwIfNotAllWalletsUnlocked && state.value.userWallets.any(UserWallet::isLocked)) {
Timber.e("Not all user wallets have been unlocked")
val userWallets = state.value.userWallets
if (throwIfNotAllWalletsUnlocked && userWallets.any(UserWallet::isLocked)) {
Timber.e("Some user wallets remain locked")
throw UserWalletsListError.NotAllUserWalletsUnlocked
}
val selectedUserWallet = selectedUserWalletSync
if (selectedUserWallet == null || selectedUserWallet.isLocked) {
Timber.e("Unable to find selected user wallet")
throw UserWalletsListError.NoUserWalletSelected
findAndSetUnlockedUserWallet(userWallets)
?: throw UserWalletsListError.NoUserWalletSelected
} else {
selectedUserWallet
}
@ -280,13 +282,14 @@ internal class BiometricUserWalletsListManager(
prevSelectedWalletId: UserWalletId?,
userWallets: List<UserWallet>,
): UserWalletId? {
val findUnlockedAndSet = {
userWallets.firstOrNull { !it.isLocked }
?.walletId
?.also { selectedUserWalletRepository.set(it) }
}
return prevSelectedWalletId
?: (selectedUserWalletRepository.get() ?: findAndSetUnlockedUserWallet(userWallets)?.walletId)
}
return prevSelectedWalletId ?: (selectedUserWalletRepository.get() ?: findUnlockedAndSet())
private fun findAndSetUnlockedUserWallet(userWallets: List<UserWallet>): UserWallet? {
return userWallets
.firstOrNull { !it.isLocked }
?.also { selectedUserWalletRepository.set(it.walletId) }
}
private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List<UserWalletId>) {

View file

@ -14,11 +14,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerW32
import com.tangem.core.ui.components.TangemSwitch
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
import com.tangem.core.ui.components.TangemSwitch
@Composable
internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) {

View file

@ -11,6 +11,7 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -111,7 +112,10 @@ class TradeCryptoMiddleware {
}
private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) {
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
?.defaultAddress
?.let(NetworkAddress.Address::value)
?: return
val status = action.cryptoCurrencyStatus
val currency = status.currency
@ -194,7 +198,10 @@ class TradeCryptoMiddleware {
}
private fun proceedNewSellAction(action: TradeCryptoAction.New.Sell) {
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
?.defaultAddress
?.let(NetworkAddress.Address::value)
?: return
val currency = action.cryptoCurrencyStatus.currency
store.state.globalState.exchangeManager.getUrl(

View file

@ -1,19 +1,18 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.jvm)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
dependencies {
/** DI */
implementation(deps.hilt.android)
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
/** Core shouldn't depends on core, but in case with utils and logging its necessary */
implementation(projects.core.utils)
implementation(projects.core.analytics.models)
implementation(deps.kotlin.coroutines)
}

View file

@ -8,4 +8,7 @@ open class AnalyticsEvent(
val event: String,
var params: Map<String, String> = mapOf(),
val error: Throwable? = null,
)
) {
val id: String = "[$category] $event"
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.analytics.models
interface OneTimeAnalyticsEvent {
val oneTimeEventId: String
}

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.core.analytics" />

View file

@ -16,7 +16,7 @@ interface AnalyticsEventFilter {
* An internal filter check that, on external or internal conditions, recognizes the possibility of
* sending an event.
*/
fun canBeSent(event: AnalyticsEvent): Boolean
suspend fun canBeSent(event: AnalyticsEvent): Boolean
/**
* Performs a check to see if the event can be dispatched by a specific handler

View file

@ -12,13 +12,11 @@ interface AnalyticsEventHandler {
interface AnalyticsHandler : AnalyticsEventHandler {
fun id(): String
fun send(event: String, params: Map<String, String> = emptyMap())
fun send(eventId: String, params: Map<String, String> = emptyMap())
override fun send(event: AnalyticsEvent) {
send(prepareEventString(event), event.params)
send(event.id, event.params)
}
fun prepareEventString(event: AnalyticsEvent): String = "[${event.category}] ${event.event}"
}
interface ErrorEventHandler {

View file

@ -2,6 +2,8 @@ package com.tangem.core.analytics.di
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.analytics.repository.AnalyticsRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -10,11 +12,16 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class AnalyticsModule {
internal object AnalyticsModule {
@Singleton
@Provides
fun provideAnalyticsHandler(): AnalyticsEventHandler {
return Analytics // todo replace after refactoring calling Analytics in whole project
}
@Provides
fun provideOneTimeEventFilter(analyticsRepository: AnalyticsRepository): OneTimeEventFilter {
return OneTimeEventFilter(analyticsRepository)
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.core.analytics.filter
import com.tangem.core.analytics.api.AnalyticsEventFilter
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.OneTimeAnalyticsEvent
import com.tangem.core.analytics.repository.AnalyticsRepository
class OneTimeEventFilter(
private val analyticsRepository: AnalyticsRepository,
) : AnalyticsEventFilter {
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is OneTimeAnalyticsEvent
override suspend fun canBeSent(event: AnalyticsEvent): Boolean {
if (event !is OneTimeAnalyticsEvent) return true
val isSent = analyticsRepository.checkIsEventSent(event.oneTimeEventId)
if (!isSent) {
analyticsRepository.setIsEventSent(event.oneTimeEventId)
}
return !isSent
}
override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean {
return canBeAppliedTo(event)
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.analytics.repository
interface AnalyticsRepository {
suspend fun checkIsEventSent(eventId: String): Boolean
suspend fun setIsEventSent(eventId: String)
}

View file

@ -37,6 +37,8 @@ object PreferencesKeys {
val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") }
val SENT_ONE_TIME_EVENTS_KEY by lazy { stringPreferencesKey(name = "sentOneTimeEvents") }
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
}

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="common_fee_error">Erhalt der Gebühr fehlgeschlagen</string>
<string name="no_account_generic">Laden Sie %1$s+ %2$s auf um ein Konto zu erstellen</string>
<string name="send_error_dust_amount_format">Minimaler Betrag ist %s</string>
<string name="send_error_dust_change">Restbestand zu klein</string>
<string name="send_error_invalid_fee_value">Falsche Gebühr</string>

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="common_fee_error">Échec de réception des commissions</string>
<string name="no_account_generic">Pour créer un compte, téléchargez %1$s+ %2$s</string>
<string name="send_error_dust_amount_format">Le montant minimal est de %s</string>
<string name="send_error_dust_change">Le reste est trop petit</string>
<string name="send_error_invalid_fee_value">Commission non valide</string>

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="common_fee_error">Impossibile ottenere la commissione</string>
<string name="no_account_generic">Scarica %1$s+ %2$s per creare un account</string>
<string name="send_error_dust_amount_format">L\'importo minimo è di %s</string>
<string name="send_error_dust_change">L\'importo residuo è molto basso</string>
<string name="send_error_invalid_fee_value">Commissione non valida</string>

View file

@ -4,7 +4,6 @@
<string name="address_type_legacy">遺留資產</string>
<string name="common_fee_error">獲取費用失敗</string>
<string name="kaspa_withdrawal_message_warning">由於 Kaspa 的限制,只有%1$d UTXO 可以放入單次交易中。這意味著您只能發送%2$s或更少數量。您需要減少數量。</string>
<string name="no_account_generic">加載 %1$s+ %2$s 以創建帳戶</string>
<string name="no_account_polkadot">目標帳戶未激活。發送 %s 或更多以激活帳戶</string>
<string name="send_error_dust_amount_format">最小數量是 %s</string>
<string name="send_error_dust_change">更動太小</string>

View file

@ -21,7 +21,7 @@ import kotlinx.coroutines.launch
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
config: TangemBottomSheetConfig,
color: Color = TangemTheme.colors.background.primary,
contentColor: Color = TangemTheme.colors.background.primary,
crossinline content: @Composable ColumnScope.(T) -> Unit,
) {
var isVisible by remember { mutableStateOf(value = config.isShow) }
@ -31,7 +31,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
ModalBottomSheet(
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = color,
containerColor = contentColor,
shape = TangemTheme.shapes.bottomSheetLarge,
dragHandle = { TangemBottomSheetDraggableHeader(color) },
) {

View file

@ -8,6 +8,7 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.SimpleSettingsRow
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@Composable
@ -24,7 +25,7 @@ private fun ChooseAddressBottomSheetContent(content: ChooseAddressBottomSheetCon
) {
content.addressModels.forEach { addressModel ->
SimpleSettingsRow(
title = addressModel.type.name,
title = addressModel.displayName.resolveReference(),
icon = R.drawable.ic_arrow_top_right_24,
onItemsClick = { content.onClick(addressModel) },
)

View file

@ -0,0 +1,43 @@
package com.tangem.core.ui.components.bottomsheets.tokenreceive
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkAddress
private const val DSC_ADDRESS_NAME = "DSC"
private const val DEL_ADDRESS_NAME = "Main"
private const val LEGACY_ADDRESS_NAME = "Legacy"
private const val DEFAULT_ADDRESS_NAME = "Default"
fun Set<NetworkAddress.Address>.mapToAddressModels(currency: CryptoCurrency): List<AddressModel> = this
.sortedBy { it.type.ordinal }
.map { address ->
AddressModel(
displayName = currency.network.getAddressDisplayName(address.type),
value = address.value,
type = when (address.type) {
NetworkAddress.Address.Type.Primary -> AddressModel.Type.Default
NetworkAddress.Address.Type.Secondary -> AddressModel.Type.Legacy
},
)
}
private fun Network.getAddressDisplayName(addressType: NetworkAddress.Address.Type): TextReference {
return when (id.value) {
"decimal", "decimal/test" -> {
when (addressType) {
NetworkAddress.Address.Type.Primary -> stringReference(value = DEL_ADDRESS_NAME)
NetworkAddress.Address.Type.Secondary -> stringReference(value = DSC_ADDRESS_NAME)
}
}
else -> {
when (addressType) {
NetworkAddress.Address.Type.Primary -> stringReference(value = DEFAULT_ADDRESS_NAME)
NetworkAddress.Address.Type.Secondary -> stringReference(value = LEGACY_ADDRESS_NAME)
}
}
}
}

View file

@ -1,8 +1,11 @@
package com.tangem.core.ui.components.bottomsheets.tokenreceive
import com.tangem.core.ui.extensions.TextReference
data class AddressModel(
val displayName: TextReference,
val value: String,
val type: Type = Type.Default,
val type: Type,
) {
enum class Type {
Legacy, Default

View file

@ -9,7 +9,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -26,6 +26,7 @@ import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.rememberQrPainters
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.shareText
import com.tangem.core.ui.res.TangemTheme
@ -181,13 +182,6 @@ private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String
return if (content.addresses.size < 2) {
content.name
} else {
"${
stringResource(
id = when (content.addresses[index].type) {
AddressModel.Type.Default -> R.string.address_type_default
AddressModel.Type.Legacy -> R.string.address_type_legacy
},
)
} ${content.name}"
"${content.addresses[index].displayName.resolveReference()} ${content.name}"
}
}

View file

@ -1,12 +1,13 @@
package com.tangem.core.ui.components.bottomsheets.tokenreceive
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import kotlinx.collections.immutable.ImmutableList
class TokenReceiveBottomSheetConfig(
val name: String,
val symbol: String,
val network: String,
val addresses: List<AddressModel>,
val addresses: ImmutableList<AddressModel>,
val onCopyClick: () -> Unit,
val onShareClick: () -> Unit,
) : TangemBottomSheetConfigContent

View file

@ -28,7 +28,6 @@ fun getActiveIconRes(blockchainId: String): Int {
"GNO" -> R.drawable.img_gnosis_22
"ETH-Pow", "ETH-Pow/test" -> R.drawable.img_eth_pow_22
"ETH-Fair" -> R.drawable.img_eth_fair_22
"NEAR", "NEAR/test" -> R.drawable.img_near_22
"Polkadot", "Polkadot/test" -> R.drawable.img_polkadot_22
"Kusama" -> R.drawable.img_kusama_22
"OPTIMISM", "OPTIMISM/test" -> R.drawable.img_optimism_22

View file

@ -32,6 +32,16 @@ fun CryptoCurrency.Token.tryGetBackgroundForTokenIcon(
): Color {
if (isGrayscale) return TangemColorPalette.Dark2
return tryGetBackgroundForTokenIcon(contractAddress = contractAddress, fallbackColor = fallbackColor)
}
/**
* Tries to extract a background color from the contract address of a token.
*
* @param fallbackColor The color to use as a fallback.
* @return The extracted background color or the fallback color if extraction fails or if it is a test network token.
*/
fun tryGetBackgroundForTokenIcon(contractAddress: String, fallbackColor: Color = TangemColorPalette.Black): Color {
return try {
val colorHex = "#" + contractAddress.substring(range = COLOR_HEX_START_INDEX..COLOR_HEX_END_INDEX)
Color(colorHex.toColorInt())

1
data/analytics/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/buld

View file

@ -0,0 +1,32 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.data.analytics"
}
dependencies {
/** Project - Analytics */
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
/** Project - Data */
implementation(projects.core.datasource)
implementation(projects.data.common)
/** AndroidX */
implementation(deps.androidx.datastore)
/** DI */
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
/** Other */
implementation(deps.kotlin.coroutines)
implementation(deps.timber)
}

View file

@ -0,0 +1,27 @@
package com.tangem.data.analytics
import com.tangem.core.analytics.repository.AnalyticsRepository
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectListSync
internal class DefaultAnalyticsRepository(
private val appPreferencesStore: AppPreferencesStore,
) : AnalyticsRepository {
override suspend fun checkIsEventSent(eventId: String): Boolean {
val sentEvents = appPreferencesStore
.getObjectListSync<String>(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY)
return eventId in sentEvents
}
override suspend fun setIsEventSent(eventId: String) {
appPreferencesStore.editData { mutablePreferences ->
val sentEvents = mutablePreferences.getObject<List<String>>(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY)
val updatedSentEvents = sentEvents.orEmpty() + eventId
mutablePreferences.setObject(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY, updatedSentEvents)
}
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.data.analytics.di
import com.tangem.core.analytics.repository.AnalyticsRepository
import com.tangem.data.analytics.DefaultAnalyticsRepository
import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal object AnalyticsDataModule {
@Provides
fun provideAnalyticsRepository(appPreferencesStore: AppPreferencesStore): AnalyticsRepository {
return DefaultAnalyticsRepository(appPreferencesStore)
}
}

View file

@ -45,4 +45,5 @@ dependencies {
implementation(deps.jodatime)
implementation(deps.timber)
implementation(deps.retrofit) // For HttpException
implementation(deps.androidx.paging.runtime)
}

View file

@ -1,11 +1,7 @@
package com.tangem.data.tokens.di
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.repository.DefaultCurrenciesRepository
import com.tangem.data.tokens.repository.DefaultMarketCryptoCurrencyRepository
import com.tangem.data.tokens.repository.DefaultNetworksRepository
import com.tangem.data.tokens.repository.DefaultQuotesRepository
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.data.tokens.repository.*
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
@ -13,10 +9,7 @@ import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.datasource.local.token.AssetsStore
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -94,4 +87,18 @@ internal object TokensDataModule {
fun provideDefaultMarketCoinsRepository(assetsStore: AssetsStore): MarketCryptoCurrencyRepository {
return DefaultMarketCryptoCurrencyRepository(assetsStore)
}
@Provides
@Singleton
fun providesTokensListRepository(
tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider,
quotesRepository: QuotesRepository,
): TokensListRepository {
return DefaultTokensListRepository(
tangemTechApi = tangemTechApi,
dispatchers = dispatchers,
quotesRepository = quotesRepository,
)
}
}

View file

@ -0,0 +1,77 @@
package com.tangem.data.tokens.paging
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.model.Token
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
/**
* A PagingSource responsible for retrieving tokens from the Tangem Tech API and adding to them quotes information.
*
* @property api Tangem Tech API
* @property quotesRepository Repository providing quotes data.
* @property dispatchers Coroutine dispatchers provider.
* @property searchText The search text used to filter tokens.
*/
internal class CoinsPagingSource(
private val api: TangemTechApi,
private val quotesRepository: QuotesRepository,
private val dispatchers: CoroutineDispatcherProvider,
private val searchText: String?,
) : PagingSource<Int, Token>() {
override fun getRefreshKey(state: PagingState<Int, Token>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(other = 1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Token> {
val page = params.key ?: 0
return com.tangem.utils.coroutines.runCatching(dispatchers.io) {
api.getCoins(
active = true, // TODO change when voting functionality is implemented
searchText = searchText,
offset = page * params.loadSize,
limit = params.loadSize,
)
}.fold(
onSuccess = { response ->
val coinsIds = response.coins.map { coin ->
CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(coin.id),
suffix = CryptoCurrency.ID.Suffix.RawID(coin.id),
)
}
val quotes = quotesRepository.getQuotesSync(currenciesIds = coinsIds.toSet(), refresh = false)
LoadResult.Page(
data = CoinsResponseConverter.convert(
CoinsData(
response.coins,
response.imageHost,
quotes,
),
),
prevKey = if (page == 0) null else page.minus(other = 1),
nextKey = if (response.coins.isEmpty()) null else page.plus(other = 1),
)
},
onFailure = { LoadResult.Error(it) },
)
}
}
data class CoinsData(
val coins: List<CoinsResponse.Coin>,
val imageHost: String?,
val quotes: Set<Quote>,
)

View file

@ -0,0 +1,51 @@
package com.tangem.data.tokens.paging
import com.tangem.blockchain.common.Blockchain
import com.tangem.data.tokens.utils.getNetworkStandardType
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Token
import com.tangem.utils.converter.Converter
/**
* Converter from data model [CoinsResponse] to list of domain models [Token]
*/
internal object CoinsResponseConverter : Converter<CoinsData, List<Token>> {
override fun convert(value: CoinsData): List<Token> {
return value.coins.map { coin ->
val id = CryptoCurrency.ID(
CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
CryptoCurrency.ID.Body.NetworkId(coin.id),
CryptoCurrency.ID.Suffix.RawID(coin.id),
)
val quote = value.quotes.find { it.rawCurrencyId == id.rawCurrencyId }
Token(
id = id.rawCurrencyId ?: coin.name,
name = coin.name,
symbol = coin.symbol,
iconUrl = getIconUrl(coin.id, value.imageHost),
isAvailable = coin.active,
networks = coin.networks.mapNotNull { network ->
val blockchain = Blockchain.fromNetworkId(network.networkId) ?: return@mapNotNull null
Token.Network(
networkId = network.networkId,
standardType = getNetworkStandardType(blockchain).name,
address = network.contractAddress,
iconUrl = getIconUrl(network.networkId, value.imageHost),
decimalCount = network.decimalCount?.toInt(),
)
},
quote = quote,
)
}
}
}
internal fun getIconUrl(id: String, imageHost: String? = null): String {
return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png"
}
private const val DEFAULT_IMAGE_HOST =
"https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"

View file

@ -0,0 +1,45 @@
package com.tangem.data.tokens.repository
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.tangem.data.tokens.paging.CoinsPagingSource
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.tokens.model.Token
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.tokens.repository.TokensListRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
/**
* Default repository implementation for managing operations related to a complete set of tokens
*
* @property tangemTechApi Tangem Tech API
* @property dispatchers coroutine dispatchers provider
* @property quotesRepository responsible for providing cryptocurrency quotes data
*
*/
internal class DefaultTokensListRepository(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
private val quotesRepository: QuotesRepository,
) : TokensListRepository {
override fun getTokens(searchText: String?): Flow<PagingData<Token>> {
return Pager(
config = PagingConfig(
pageSize = 100,
prefetchDistance = 70,
enablePlaceholders = false,
),
pagingSourceFactory = {
CoinsPagingSource(
api = tangemTechApi,
dispatchers = dispatchers,
searchText = searchText,
quotesRepository = quotesRepository,
)
},
).flow
}
}

View file

@ -53,7 +53,7 @@ private fun getNetworkDerivationPath(
}
}
private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
internal fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
return when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20
Blockchain.BSC, Blockchain.BSCTestnet -> Network.StandardType.BEP20

View file

@ -2,6 +2,7 @@ package com.tangem.data.tokens.utils
import com.tangem.domain.tokens.model.*
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.walletmanager.model.Address
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
@ -20,12 +21,12 @@ internal class NetworkStatusFactory {
is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation
is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(
address = getNetworkAddress(result.defaultAddress, result.addresses),
address = getNetworkAddress(result.selectedAddress, result.addresses),
amountToCreateAccount = result.amountToCreateAccount,
errorMessage = result.errorMessage,
)
is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified(
address = getNetworkAddress(result.defaultAddress, result.addresses),
address = getNetworkAddress(result.selectedAddress, result.addresses),
amounts = formatAmounts(result.currenciesAmounts, currencies),
pendingTransactions = formatTransactions(
transactions = result.currentTransactions,
@ -92,11 +93,26 @@ internal class NetworkStatusFactory {
return transactions.mapTo(hashSetOf()) { it.txHistoryItem }
}
private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set<String>): NetworkAddress {
private fun getNetworkAddress(selectedAddress: String, availableAddresses: Set<Address>): NetworkAddress {
val defaultAddress = availableAddresses
.firstOrNull { it.value == selectedAddress }
?.let(::mapToDomainAddress)
requireNotNull(defaultAddress) { "Selected address must not be null" }
return if (availableAddresses.size != 1) {
NetworkAddress.Selectable(defaultAddress, availableAddresses)
NetworkAddress.Selectable(defaultAddress, availableAddresses.mapTo(hashSetOf(), ::mapToDomainAddress))
} else {
NetworkAddress.Single(defaultAddress)
}
}
private fun mapToDomainAddress(address: Address): NetworkAddress.Address {
val type = when (address.type) {
Address.Type.Primary -> NetworkAddress.Address.Type.Primary
Address.Type.Secondary -> NetworkAddress.Address.Type.Secondary
}
return NetworkAddress.Address(address.value, type)
}
}

View file

@ -195,8 +195,7 @@ fun Blockchain.toCoinId(): String {
Blockchain.Chia, Blockchain.ChiaTestnet -> "chia"
Blockchain.Near -> "near"
Blockchain.NearTestnet -> "near/test"
Blockchain.Decimal -> "decimal"
Blockchain.DecimalTestnet -> "decimal/test"
Blockchain.Decimal, Blockchain.DecimalTestnet -> "decimal"
Blockchain.Unknown -> "unknown"
}
}
@ -224,6 +223,4 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5
private val excludedBlockchains = listOf(
Blockchain.Unknown,
Blockchain.Ducatus,
Blockchain.Decimal,
Blockchain.DecimalTestnet,
)

View file

@ -116,6 +116,7 @@ interface WalletManagersFacade {
* @param userWalletId selected wallet id
* @param network network of currency
*/
@Deprecated("Use NetworkAddress from CryptoCurrencyStatus")
suspend fun getAddress(userWalletId: UserWalletId, network: Network): List<Address>
/** Returns list of all addresses for all currencies in selected wallet
@ -123,6 +124,7 @@ interface WalletManagersFacade {
* @param userWalletId selected wallet id
* @param network required to create wallet manager
*/
@Deprecated("Use NetworkAddress from CryptoCurrencyStatus")
suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set<Address>
/**
@ -156,6 +158,7 @@ interface WalletManagersFacade {
* @param userWalletId selected wallet id
* @param network network of currency
*/
@Deprecated("Will be removed in future")
suspend fun getFee(
amount: Amount,
destination: String,
@ -171,6 +174,7 @@ interface WalletManagersFacade {
* @param userWalletId selected wallet id
* @param network network of currency
*/
@Deprecated("Will be removed in future")
suspend fun validateTransaction(
amount: Amount,
fee: Amount?,
@ -189,6 +193,7 @@ interface WalletManagersFacade {
* @param network network of currency
*/
@Suppress("LongParameterList")
@Deprecated("Will be removed in future")
suspend fun createTransaction(
amount: Amount,
fee: Fee,
@ -206,6 +211,7 @@ interface WalletManagersFacade {
* @param userWalletId selected wallet id
* @param network network of currency
*/
@Deprecated("Will be removed in future")
suspend fun sendTransaction(
txData: TransactionData,
signer: CommonSigner,

View file

@ -0,0 +1,11 @@
package com.tangem.domain.walletmanager.model
data class Address(
val value: String,
val type: Type,
) {
enum class Type {
Primary, Secondary,
}
}

View file

@ -9,15 +9,15 @@ sealed class UpdateWalletManagerResult {
object Unreachable : UpdateWalletManagerResult()
data class Verified(
val defaultAddress: String,
val addresses: Set<String>,
val selectedAddress: String,
val addresses: Set<Address>,
val currenciesAmounts: Set<CryptoCurrencyAmount>,
val currentTransactions: Set<CryptoCurrencyTransaction>,
) : UpdateWalletManagerResult()
data class NoAccount(
val defaultAddress: String,
val addresses: Set<String>,
val selectedAddress: String,
val addresses: Set<Address>,
val amountToCreateAccount: BigDecimal,
val errorMessage: String,
) : UpdateWalletManagerResult()

View file

@ -1,15 +1,17 @@
package com.tangem.domain.walletmanager.utils
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.address.AddressType
import com.tangem.domain.common.extensions.amountToCreateAccount
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.walletmanager.model.Address
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
import timber.log.Timber
import java.math.BigDecimal
import java.util.concurrent.TimeUnit
import com.tangem.blockchain.common.address.Address as SdkAddress
internal class UpdateWalletManagerResultFactory {
@ -18,7 +20,7 @@ internal class UpdateWalletManagerResultFactory {
val addresses = getAvailableAddresses(wallet.addresses)
return UpdateWalletManagerResult.Verified(
defaultAddress = wallet.address,
selectedAddress = wallet.address,
addresses = addresses,
currenciesAmounts = getTokensAmounts(wallet.amounts.values.toSet()),
currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()),
@ -30,7 +32,7 @@ internal class UpdateWalletManagerResultFactory {
val addresses = getAvailableAddresses(wallet.addresses)
return UpdateWalletManagerResult.Verified(
defaultAddress = wallet.address,
selectedAddress = wallet.address,
addresses = addresses,
currenciesAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens),
currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()),
@ -48,7 +50,7 @@ internal class UpdateWalletManagerResultFactory {
UpdateWalletManagerResult.Unreachable
} else {
UpdateWalletManagerResult.NoAccount(
defaultAddress = wallet.address,
selectedAddress = wallet.address,
addresses = getAvailableAddresses(wallet.addresses),
amountToCreateAccount = amountToCreateAccount,
errorMessage = customMessage,
@ -70,7 +72,7 @@ internal class UpdateWalletManagerResultFactory {
}
private fun getCurrentTransactions(
walletAddresses: Set<String>,
walletAddresses: Set<Address>,
recentTransactions: Set<TransactionData>,
): Set<CryptoCurrencyTransaction> {
val unconfirmedTransactions = recentTransactions.filter {
@ -95,7 +97,7 @@ internal class UpdateWalletManagerResultFactory {
}
private fun createCurrencyTransaction(
walletAddresses: Set<String>,
walletAddresses: Set<Address>,
data: TransactionData,
): CryptoCurrencyTransaction? {
return when (val type = data.amount.type) {
@ -115,11 +117,11 @@ internal class UpdateWalletManagerResultFactory {
}
}
private fun createTxHistoryItem(walletAddresses: Set<String>, data: TransactionData): TxHistoryItem? {
private fun createTxHistoryItem(walletAddresses: Set<Address>, data: TransactionData): TxHistoryItem? {
val hash = data.hash ?: return null
val millis = data.date?.timeInMillis ?: return null
val amount = getTransactionAmountValue(data.amount) ?: return null
val isOutgoing = data.sourceAddress in walletAddresses
val isOutgoing = data.sourceAddress in walletAddresses.map { it.value }
return TxHistoryItem(
txHash = hash,
@ -141,8 +143,16 @@ internal class UpdateWalletManagerResultFactory {
)
}
private fun getAvailableAddresses(addresses: Set<Address>): Set<String> {
return addresses.mapTo(hashSetOf()) { it.value }
private fun getAvailableAddresses(addresses: Set<SdkAddress>): Set<Address> {
return addresses.mapTo(hashSetOf()) { sdkAddress ->
Address(
value = sdkAddress.value,
type = when (sdkAddress.type) {
AddressType.Default -> Address.Type.Primary
AddressType.Legacy -> Address.Type.Secondary
},
)
}
}
private fun getCurrencyAmountValue(amount: Amount): BigDecimal? {

View file

@ -22,6 +22,9 @@ dependencies {
/** Project - Other */
implementation(projects.core.utils)
/** Android - Other */
implementation(deps.androidx.paging.runtime)
/** Utils */
implementation(deps.jodatime)
implementation(deps.reKotlin)

View file

@ -78,7 +78,7 @@ data class CryptoCurrencyStatus(
override val fiatAmount: BigDecimal?,
override val priceChange: BigDecimal?,
override val fiatRate: BigDecimal?,
override val networkAddress: NetworkAddress?,
override val networkAddress: NetworkAddress,
) : Status(isError = false) {
override val amount: BigDecimal = BigDecimal.ZERO
@ -102,7 +102,7 @@ data class CryptoCurrencyStatus(
override val priceChange: BigDecimal,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<TxHistoryItem>,
override val networkAddress: NetworkAddress?,
override val networkAddress: NetworkAddress,
) : Status(isError = false)
/**
@ -123,7 +123,7 @@ data class CryptoCurrencyStatus(
override val priceChange: BigDecimal?,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<TxHistoryItem>,
override val networkAddress: NetworkAddress?,
override val networkAddress: NetworkAddress,
) : Status(isError = false)
/**
@ -138,6 +138,6 @@ data class CryptoCurrencyStatus(
override val amount: BigDecimal,
override val hasCurrentNetworkTransactions: Boolean,
override val pendingTransactions: Set<TxHistoryItem>,
override val networkAddress: NetworkAddress?,
override val networkAddress: NetworkAddress,
) : Status(isError = false)
}

View file

@ -6,18 +6,19 @@ package com.tangem.domain.tokens.model
sealed class NetworkAddress {
/** The default or currently selected network address. */
abstract val defaultAddress: String
abstract val defaultAddress: Address
/** The set of available network addresses to choose from. */
abstract val availableAddresses: Set<Address>
/**
* Represents a single static network address.
*
* @property defaultAddress The static network address.
*/
data class Single(override val defaultAddress: String) : NetworkAddress() {
data class Single(override val defaultAddress: Address) : NetworkAddress() {
init {
checkDefaultAddress()
}
override val availableAddresses: Set<Address> = setOf(defaultAddress)
}
/**
@ -27,17 +28,26 @@ sealed class NetworkAddress {
* @property availableAddresses The set of available network addresses to choose from.
*/
data class Selectable(
override val defaultAddress: String,
val availableAddresses: Set<String>,
override val defaultAddress: Address,
override val availableAddresses: Set<Address>,
) : NetworkAddress() {
init {
checkDefaultAddress()
require(availableAddresses.isNotEmpty()) { "Available network addresses must not be empty" }
}
}
protected fun checkDefaultAddress() {
require(defaultAddress.isNotBlank()) { "Selected network address must not be blank" }
data class Address(
val value: String,
val type: Type,
) {
enum class Type {
Primary, Secondary,
}
init {
require(value.isNotBlank()) { "Address value must not be blank" }
}
}
}

View file

@ -3,14 +3,17 @@ package com.tangem.domain.tokens.model
import java.math.BigDecimal
/**
* Represents a financial quote for a specific cryptocurrency, including its fiat exchange rate and price change.
* Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change.
*
* @property rawCurrencyId The unique identifier of the token for which the quote is provided.
* @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided.
* @property fiatRate The current fiat exchange rate for the cryptocurrency.
* @property priceChange The price change for the cryptocurrency.
* @property values The values representing the cryptocurrency's price changes over a 24-hour period,
* suitable for chart plotting.
*/
data class Quote(
val rawCurrencyId: String,
val fiatRate: BigDecimal,
val priceChange: BigDecimal,
val values: List<Double>? = null,
)

View file

@ -0,0 +1,41 @@
package com.tangem.domain.tokens.model
/**
* Represents a domain model for a token (as part of a general list of tokens).
*
* @property id The unique identifier of the token.
* @property networks List of networks associated with the token.
* @property isAvailable Indicates whether this token is supported in Tangem app.
* @property quote Equivalent prices for the token.
* @property name The name of the token.
* @property symbol The brief name of the token, e.g., "BTC".
* @property iconUrl URL of the token's icon.
*/
data class Token(
val id: String,
val networks: List<Network>,
val isAvailable: Boolean,
val quote: Quote?,
val name: String,
val symbol: String,
val iconUrl: String,
) {
/**
* Represents a domain model for a network associated with a token.
*
* @property networkId The unique identifier of the network.
* @property standardType The type of blockchain associated with the network.
* @property address The contract address of the token on the current network.
* It is null for the main currencies of the network.
* @property iconUrl URL of the network's icon.
* @property decimalCount The decimal count associated with the token on the network.
*/
data class Network(
val networkId: String,
val standardType: String,
val address: String?,
val iconUrl: String,
val decimalCount: Int?,
)
}

View file

@ -0,0 +1,22 @@
package com.tangem.domain.tokens
import androidx.paging.PagingData
import com.tangem.domain.tokens.model.Token
import com.tangem.domain.tokens.repository.TokensListRepository
import kotlinx.coroutines.flow.Flow
/**
* Use case for retrieving a complete set of tokens with their quotes (filtered by a search text if provided).
*
* @property repository The repository responsible for fetching and preparing the list of tokens with their quotes.
*/
class GetGlobalTokenListUseCase(private val repository: TokensListRepository) {
/**
* @param searchText The text used for filtering the list of tokens (not required).
* @return A [Flow] emitting [PagingData] containing the tokens with their quotes.
*/
operator fun invoke(searchText: String?): Flow<PagingData<Token>> {
return repository.getTokens(searchText = searchText?.ifBlank(defaultValue = { null }))
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.domain.tokens.repository
import androidx.paging.PagingData
import com.tangem.domain.tokens.model.Token
import kotlinx.coroutines.flow.Flow
/**
* Repository interface for managing operations related to a complete set of tokens
* (not associated with any specific card).
* */
interface TokensListRepository {
/**
* Retrieves a list of available tokens with quotes, can be filtered based on the provided search text.
*
* @param searchText The search text used to filter tokens.
* @return A [Flow] emitting [PagingData] containing the tokens with quotes matching the search criteria.
*/
fun getTokens(searchText: String?): Flow<PagingData<Token>>
}

View file

@ -118,7 +118,9 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
amount = BigDecimal.TEN,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = NetworkAddress.Single(defaultAddress = "mock"),
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
),
)
}

View file

@ -57,7 +57,9 @@ internal object MockNetworks {
network = network3,
value = NetworkStatus.NoAccount(
amountToCreateAccount = amountToCreateAccount,
address = NetworkAddress.Single(defaultAddress = "mock"),
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
errorMessage = "",
),
)
@ -73,7 +75,9 @@ internal object MockNetworks {
MockTokens.token3.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
),
pendingTransactions = emptyMap(),
address = NetworkAddress.Single(defaultAddress = "mock"),
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
),
)
@ -86,7 +90,9 @@ internal object MockNetworks {
MockTokens.token6.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
),
pendingTransactions = emptyMap(),
address = NetworkAddress.Single(defaultAddress = "mock"),
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
),
)
@ -100,7 +106,9 @@ internal object MockNetworks {
MockTokens.token10.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
),
pendingTransactions = emptyMap(),
address = NetworkAddress.Single(defaultAddress = "mock"),
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
),
)

View file

@ -3,6 +3,7 @@ package com.tangem.domain.tokens.mock
import arrow.core.nonEmptyListOf
import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.NetworkStatus
import java.math.BigDecimal
@ -64,7 +65,9 @@ internal object MockTokensStates {
priceChange = MockQuotes.quote7.priceChange,
fiatRate = MockQuotes.quote7.fiatRate,
amountToCreateAccount = MockNetworks.amountToCreateAccount,
networkAddress = null,
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
),
)
@ -75,7 +78,9 @@ internal object MockTokensStates {
priceChange = MockQuotes.quote8.priceChange,
fiatRate = MockQuotes.quote8.fiatRate,
amountToCreateAccount = MockNetworks.amountToCreateAccount,
networkAddress = null,
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
),
)
@ -86,7 +91,9 @@ internal object MockTokensStates {
priceChange = MockQuotes.quote9.priceChange,
fiatRate = MockQuotes.quote9.fiatRate,
amountToCreateAccount = MockNetworks.amountToCreateAccount,
networkAddress = null,
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
),
)
@ -97,7 +104,9 @@ internal object MockTokensStates {
priceChange = MockQuotes.quote10.priceChange,
fiatRate = MockQuotes.quote10.fiatRate,
amountToCreateAccount = MockNetworks.amountToCreateAccount,
networkAddress = null,
networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
),
)

View file

@ -47,6 +47,9 @@ dependencies {
implementation(projects.core.ui)
implementation(projects.core.utils)
/** Project - Data */
implementation(projects.data.tokens)
/** Domain modules */
implementation(projects.common)
implementation(projects.domain.card)

View file

@ -0,0 +1,22 @@
package com.tangem.managetokens.presentation.common.ui
import androidx.compose.runtime.Composable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
@Composable
internal fun ChooseWalletBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<ChooseWalletBottomSheetConfig>(
config = config,
contentColor = TangemTheme.colors.background.tertiary,
) {
ChooseWalletScreen(state = it.chooseWalletState)
}
}
internal class ChooseWalletBottomSheetConfig(
val chooseWalletState: ChooseWalletState.Choose,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,24 @@
package com.tangem.managetokens.presentation.managetokens.state
import androidx.paging.PagingData
import com.tangem.core.ui.event.StateEvent
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
import com.tangem.managetokens.presentation.common.state.Event
import kotlinx.coroutines.flow.Flow
internal data class ManageTokensState(
val searchBarState: SearchBarState,
val tokens: Flow<PagingData<TokenItemState>>,
val isLoading: Boolean,
val addCustomTokenButton: AddCustomTokenButton,
val chooseWalletState: ChooseWalletState,
val derivationNotification: DerivationNotificationState? = null,
val selectedToken: TokenItemState.Loaded? = null,
val showChooseWalletScreen: Boolean = false,
val event: StateEvent<Event>,
)
data class AddCustomTokenButton(
val isVisible: Boolean,
val onClick: () -> Unit,
)

View file

@ -0,0 +1,40 @@
package com.tangem.managetokens.presentation.managetokens.state.previewdata
import androidx.paging.PagingData
import com.tangem.core.ui.event.consumedEvent
import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData
import com.tangem.managetokens.presentation.managetokens.state.AddCustomTokenButton
import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState
import com.tangem.managetokens.presentation.managetokens.state.SearchBarState
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
import kotlinx.coroutines.flow.flowOf
internal object ManageTokensStatePreviewData {
val loadedState: ManageTokensState
get() = ManageTokensState(
searchBarState = searchState,
tokens = flowOf(PagingData.from(tokens)),
isLoading = false,
addCustomTokenButton = AddCustomTokenButton(true, {}),
derivationNotification = DerivationNotificationStatePreviewData.state,
event = consumedEvent(),
chooseWalletState = ChooseWalletStatePreviewData.state,
)
val loadingState: ManageTokensState
get() = loadedState.copy(isLoading = true)
private val tokens: List<TokenItemState>
get() = listOf(
TokenItemStatePreviewData.loadedPriceDown,
TokenItemStatePreviewData.loadedPriceUp,
)
private val searchState: SearchBarState
get() = SearchBarState(
query = "",
onQueryChange = {},
active = false,
onActiveChange = {},
)
}

View file

@ -0,0 +1,24 @@
package com.tangem.managetokens.presentation.managetokens.ui
import androidx.compose.runtime.Composable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
@Composable
internal fun ChooseNetworkBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<ChooseNetworkBottomSheetConfig>(
config = config,
contentColor = TangemTheme.colors.background.tertiary,
) {
ChooseNetworkScreen(state = it.selectedToken, walletState = it.chooseWalletState)
}
}
internal class ChooseNetworkBottomSheetConfig(
val selectedToken: TokenItemState.Loaded,
val chooseWalletState: ChooseWalletState,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,144 @@
package com.tangem.managetokens.presentation.managetokens.ui
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
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 androidx.paging.compose.collectAsLazyPagingItems
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.res.TangemTheme
import com.tangem.managetokens.presentation.common.state.AlertState
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheet
import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheetConfig
import com.tangem.managetokens.presentation.common.ui.EventEffect
import com.tangem.managetokens.presentation.common.ui.components.Alert
import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
import com.tangem.managetokens.presentation.managetokens.state.previewdata.ManageTokensStatePreviewData
import com.tangem.managetokens.presentation.managetokens.ui.components.DerivationNotification
import com.tangem.managetokens.presentation.managetokens.ui.components.TokensList
import com.tangem.managetokens.presentation.managetokens.ui.components.TokensSearchBar
@Composable
internal fun ManageTokensScreen(state: ManageTokensState) {
var alertState by remember { mutableStateOf<AlertState?>(value = null) }
EventEffect(
event = state.event,
onAlertStateSet = { alertState = it },
)
alertState?.let {
Alert(state = it, onDismiss = { alertState = null })
}
Content(state)
}
@Composable
private fun Content(state: ManageTokensState) {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = TangemTheme.colors.background.primary)
.padding(top = TangemTheme.dimens.spacing32),
) {
Column {
val listState = rememberLazyListState()
val raiseSearchBar by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }
val elevation by animateDpAsState(
targetValue = if (raiseSearchBar) {
TangemTheme.dimens.elevation8
} else {
TangemTheme.dimens.elevation0
},
label = "top_bar_elevation",
)
Surface(
elevation = elevation,
modifier = Modifier,
) {
TokensSearchBar(
state = state.searchBarState,
modifier = Modifier
.background(color = TangemTheme.colors.background.primary)
.padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing20),
)
}
val tokens = state.tokens.collectAsLazyPagingItems()
TokensList(tokens = tokens, addCustomTokenButton = state.addCustomTokenButton)
}
state.derivationNotification?.let {
DerivationNotification(
config = it.config,
modifier = Modifier
.align(Alignment.BottomCenter),
)
}
state.selectedToken?.let { selectedToken ->
ManageTokensBottomSheet(selectedToken = selectedToken, state = state)
}
}
}
@Composable
private fun ManageTokensBottomSheet(selectedToken: TokenItemState.Loaded, state: ManageTokensState) {
if (state.showChooseWalletScreen && state.chooseWalletState is ChooseWalletState.Choose) {
val config = TangemBottomSheetConfig(
isShow = true,
content = ChooseWalletBottomSheetConfig(state.chooseWalletState),
onDismissRequest = state.chooseWalletState.onCloseChoosingWalletClick,
)
ChooseWalletBottomSheet(config)
} else {
val config = TangemBottomSheetConfig(
isShow = true,
content = ChooseNetworkBottomSheetConfig(
selectedToken = selectedToken,
chooseWalletState = state.chooseWalletState,
),
onDismissRequest = selectedToken.chooseNetworkState.onCloseChooseNetworkScreen,
)
ChooseNetworkBottomSheet(config)
}
}
@Preview
@Composable
private fun Preview_ManageTokensScreen_LightTheme(
@PreviewParameter(ManageTokensConfigProvider::class)
state: ManageTokensState,
) {
TangemTheme(isDark = false) {
ManageTokensScreen(state)
}
}
@Preview
@Composable
private fun Preview_ManageTokensScreen_DarkTheme(
@PreviewParameter(ManageTokensConfigProvider::class)
state: ManageTokensState,
) {
TangemTheme(isDark = true) {
ManageTokensScreen(state)
}
}
private class ManageTokensConfigProvider : CollectionPreviewParameterProvider<ManageTokensState>(
collection = listOf(
ManageTokensStatePreviewData.loadingState,
ManageTokensStatePreviewData.loadedState,
),
)

View file

@ -0,0 +1,65 @@
package com.tangem.managetokens.presentation.managetokens.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.managetokens.impl.R
@Composable
internal fun AddCustomTokenButton(onButtonClick: () -> Unit, modifier: Modifier = Modifier) {
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size68)
.fillMaxWidth()
.clickable { onButtonClick() }
.padding(horizontal = TangemTheme.dimens.spacing16),
) {
Box(
modifier = Modifier
.size(TangemTheme.dimens.size36)
.background(color = TangemTheme.colors.button.secondary, shape = CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_plus_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
SpacerW(width = TangemTheme.dimens.spacing12)
Text(
text = stringResource(id = R.string.add_custom_token_title),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Preview
@Composable
private fun AddCustomTokenButton_Preview_Light() {
TangemTheme(isDark = false) {
AddCustomTokenButton(onButtonClick = { })
}
}
@Preview
@Composable
private fun AddCustomTokenButton_Preview_Dark() {
TangemTheme(isDark = true) {
AddCustomTokenButton(onButtonClick = { })
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.managetokens.presentation.managetokens.ui.components
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.managetokens.impl.R
import com.tangem.managetokens.presentation.managetokens.state.AddCustomTokenButton
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
private const val PLACEHOLDER_ITEMS_COUNT = 50
@Composable
internal fun TokensList(tokens: LazyPagingItems<TokenItemState>, addCustomTokenButton: AddCustomTokenButton) {
LazyColumn {
item {
Text(
text = stringResource(id = R.string.manage_tokens_title),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
if (tokens.loadState.refresh is LoadState.Loading) {
items(PLACEHOLDER_ITEMS_COUNT) {
TokenRowItem(state = TokenItemState.Loading(it.toString()))
}
} else {
val tokensList = tokens.itemSnapshotList
if (tokensList.isEmpty()) {
item {
Text(
text = stringResource(id = R.string.manage_tokens_nothing_found),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
}
items(items = tokensList.items, key = TokenItemState::id) { token ->
TokenRowItem(state = token)
}
if (addCustomTokenButton.isVisible) {
item {
AddCustomTokenButton(onButtonClick = { addCustomTokenButton.onClick })
}
}
}
}
}

View file

@ -2,12 +2,11 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import androidx.paging.PagingData
import arrow.core.Either
import com.tangem.blockchain.common.address.Address
import com.tangem.common.Provider
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.consumedEvent
@ -17,6 +16,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.txhistory.models.TxHistoryItem
@ -176,7 +176,7 @@ internal class TokenDetailsStateFactory(
fun getStateWithReceiveBottomSheet(
currency: CryptoCurrency,
addresses: List<Address>,
networkAddress: NetworkAddress,
sendCopyAnalyticsEvent: () -> Unit,
sendShareAnalyticsEvent: () -> Unit,
): TokenDetailsState {
@ -188,12 +188,7 @@ internal class TokenDetailsStateFactory(
name = currency.name,
symbol = currency.symbol,
network = currency.network.name,
addresses = addresses.map {
AddressModel(
value = it.value,
type = AddressModel.Type.valueOf(it.type.name),
)
},
addresses = networkAddress.availableAddresses.mapToAddressModels(currency).toImmutableList(),
onCopyClick = sendCopyAnalyticsEvent,
onShareClick = sendShareAnalyticsEvent,
),
@ -201,20 +196,16 @@ internal class TokenDetailsStateFactory(
)
}
fun getStateWithChooseAddressBottomSheet(addresses: List<Address>): TokenDetailsState {
fun getStateWithChooseAddressBottomSheet(
currency: CryptoCurrency,
networkAddress: NetworkAddress,
): TokenDetailsState {
return currentStateProvider().copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissBottomSheet,
content = ChooseAddressBottomSheetConfig(
addressModels = addresses
.map { address ->
AddressModel(
value = address.value,
type = AddressModel.Type.valueOf(address.type.name),
)
}
.toImmutableList(),
addressModels = networkAddress.availableAddresses.mapToAddressModels(currency).toImmutableList(),
onClick = clickIntents::onAddressTypeSelected,
),
),

View file

@ -23,13 +23,13 @@ import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent
import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent
import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
@ -367,19 +367,15 @@ internal class TokenDetailsViewModel @Inject constructor(
}
override fun onReceiveClick() {
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol))
val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return
viewModelScope.launch(dispatchers.io) {
val addresses = walletManagersFacade.getAddress(
userWalletId = userWalletId,
network = cryptoCurrency.network,
)
analyticsEventsHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened)
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol))
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened)
uiState = stateFactory.getStateWithReceiveBottomSheet(
currency = cryptoCurrency,
addresses = addresses,
networkAddress = networkAddress,
sendCopyAnalyticsEvent = {
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol))
},
@ -442,22 +438,23 @@ internal class TokenDetailsViewModel @Inject constructor(
}
private fun openExplorer() {
viewModelScope.launch(dispatchers.io) {
val addresses = walletManagersFacade.getAddress(
userWalletId = userWalletId,
network = cryptoCurrency.network,
)
val currencyStatus = cryptoCurrencyStatus ?: return
if (addresses.size == 1) {
router.openUrl(
url = getExploreUrlUseCase(
userWalletId = userWalletId,
currency = cryptoCurrency,
addressType = AddressType.Default,
),
)
} else {
uiState = stateFactory.getStateWithChooseAddressBottomSheet(addresses = addresses)
viewModelScope.launch(dispatchers.io) {
when (val addresses = currencyStatus.value.networkAddress) {
is NetworkAddress.Selectable -> {
uiState = stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses)
}
is NetworkAddress.Single -> {
router.openUrl(
url = getExploreUrlUseCase(
userWalletId = userWalletId,
currency = cryptoCurrency,
addressType = AddressType.Default,
),
)
}
null -> Unit
}
}
}

View file

@ -54,17 +54,18 @@ internal class DefaultWalletRouter(
) {
composable(WalletRoute.Wallet.route) {
if (walletFeatureToggles.isWalletsScrollingPreviewEnabled) {
val viewModel = hiltViewModel<WalletViewModel>().apply {
router = this@DefaultWalletRouter
}
WalletScreen(state = viewModel.uiState)
} else {
val viewModel = hiltViewModel<WalletViewModelV2>().apply {
setWalletRouter(router = this@DefaultWalletRouter)
}
WalletScreenV2(state = viewModel.uiState.collectAsStateWithLifecycle().value)
} else {
val viewModel = hiltViewModel<WalletViewModel>().apply {
router = this@DefaultWalletRouter
}
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
WalletScreen(state = viewModel.uiState)
}
}

View file

@ -19,6 +19,7 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.WrappedList
@ -42,10 +43,7 @@ import com.tangem.domain.settings.*
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent
import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
@ -689,20 +687,11 @@ internal class WalletViewModel @Inject constructor(
}
override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
val state = uiState as? WalletState.ContentState ?: return
analyticsEventsHandler.send(
event = TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrencyStatus.currency.symbol),
)
viewModelScope.launch(dispatchers.io) {
val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex)
val addresses = walletManagersFacade.getAddress(
userWalletId = userWallet.walletId,
network = cryptoCurrencyStatus.currency.network,
)
analyticsEventsHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened)
val currency = cryptoCurrencyStatus.currency
@ -711,12 +700,11 @@ internal class WalletViewModel @Inject constructor(
name = currency.name,
symbol = currency.symbol,
network = currency.network.name,
addresses = addresses.map {
AddressModel(
value = it.value,
type = AddressModel.Type.valueOf(it.type.name),
)
},
addresses = cryptoCurrencyStatus.value.networkAddress
?.availableAddresses
?.mapToAddressModels(currency)
.orEmpty()
.toImmutableList(),
onCopyClick = {
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol))
},
@ -821,41 +809,39 @@ internal class WalletViewModel @Inject constructor(
private fun openExplorer() {
val state = uiState as? WalletState.ContentState ?: return
val currency = singleWalletCryptoCurrencyStatus?.currency ?: return
val currencyStatus = singleWalletCryptoCurrencyStatus ?: return
val currency = currencyStatus.currency
viewModelScope.launch(dispatchers.main) {
val userWalletId = getWallet(state.walletsListConfig.selectedWalletIndex).walletId
val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network)
if (addresses.size == 1) {
router.openUrl(
url = getExploreUrlUseCase(
userWalletId = userWalletId,
currency = currency,
addressType = AddressType.Default,
),
)
} else {
uiState = stateFactory.getStateWithOpenWalletBottomSheet(
ChooseAddressBottomSheetConfig(
addressModels = addresses
.map { address ->
AddressModel(
value = address.value,
type = AddressModel.Type.valueOf(address.type.name),
when (val addresses = currencyStatus.value.networkAddress) {
is NetworkAddress.Selectable -> {
uiState = stateFactory.getStateWithOpenWalletBottomSheet(
ChooseAddressBottomSheetConfig(
addressModels = addresses.availableAddresses
.mapToAddressModels(currency)
.toImmutableList(),
onClick = {
onAddressTypeSelected(
userWalletId = userWalletId,
currency = currency,
addressModel = it,
)
}
.toImmutableList(),
onClick = {
onAddressTypeSelected(
userWalletId = userWalletId,
currency = currency,
addressModel = it,
)
},
),
)
},
),
)
}
is NetworkAddress.Single -> {
router.openUrl(
url = getExploreUrlUseCase(
userWalletId = userWalletId,
currency = currency,
addressType = AddressType.Default,
),
)
}
null -> Unit
}
}
}

View file

@ -1,12 +1,12 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.address.AddressType
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels
import com.tangem.core.ui.extensions.WrappedList
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -20,6 +20,7 @@ import com.tangem.domain.tokens.RemoveCurrencyUseCase
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent
import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent
import com.tangem.domain.walletconnect.WalletConnectActions
@ -141,15 +142,15 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
)
viewModelScope.launch(dispatchers.main) {
val currency = cryptoCurrencyStatus.currency
val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network)
analyticsEventHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened)
stateHolder.update(
OpenBottomSheetTransformer(
userWalletId = userWalletId,
content = createReceiveBottomSheetContent(currency, addresses),
content = createReceiveBottomSheetContent(
currency = cryptoCurrencyStatus.currency,
addresses = cryptoCurrencyStatus.value.networkAddress?.availableAddresses ?: return@launch,
),
onDismissBottomSheet = {
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId))
},
@ -160,18 +161,13 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private fun createReceiveBottomSheetContent(
currency: CryptoCurrency,
addresses: List<Address>,
addresses: Set<NetworkAddress.Address>,
): TangemBottomSheetConfigContent {
return TokenReceiveBottomSheetConfig(
name = currency.name,
symbol = currency.symbol,
network = currency.network.name,
addresses = addresses.map { address ->
AddressModel(
value = address.value,
type = AddressModel.Type.valueOf(address.type.name),
)
},
addresses = addresses.mapToAddressModels(currency).toImmutableList(),
onCopyClick = {
analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol))
},
@ -329,40 +325,36 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
val userWalletId = stateHolder.getSelectedWalletId()
viewModelScope.launch(dispatchers.main) {
val currency = getPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId)?.currency ?: return@launch
val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network)
val currencyStatus = getPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId) ?: return@launch
if (addresses.size == 1) {
router.openUrl(
url = getExploreUrlUseCase(
userWalletId = userWalletId,
currency = currency,
addressType = AddressType.Default,
),
)
} else {
showChooseAddressBottomSheet(userWalletId, addresses, currency)
when (val addresses = currencyStatus.value.networkAddress) {
is NetworkAddress.Selectable -> {
showChooseAddressBottomSheet(userWalletId, addresses.availableAddresses, currencyStatus.currency)
}
is NetworkAddress.Single -> {
router.openUrl(
url = getExploreUrlUseCase(
userWalletId = userWalletId,
currency = currencyStatus.currency,
addressType = AddressType.Default,
),
)
}
null -> Unit
}
}
}
private fun showChooseAddressBottomSheet(
userWalletId: UserWalletId,
addresses: List<Address>,
addresses: Set<NetworkAddress.Address>,
currency: CryptoCurrency,
) {
stateHolder.update(
OpenBottomSheetTransformer(
userWalletId = userWalletId,
content = ChooseAddressBottomSheetConfig(
addressModels = addresses
.map { address ->
AddressModel(
value = address.value,
type = AddressModel.Type.valueOf(address.type.name),
)
}
.toImmutableList(),
addressModels = addresses.mapToAddressModels(currency).toImmutableList(),
onClick = {
onAddressTypeSelected(
userWalletId = userWalletId,

View file

@ -88,10 +88,10 @@ spr-client = "3.6.2"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-396"
tangemBlockchainSdk = "develop-401"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-312"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
# endregion Tangem
# region Tools

View file

@ -128,7 +128,6 @@ include(":domain:app-theme:models")
include(":domain:balance-hiding")
include(":domain:balance-hiding:models")
include(":domain:transaction")
// endregion Domain modules
// region Data modules
@ -142,4 +141,5 @@ include(":data:source:preferences")
include(":data:settings")
include(":data:txhistory")
include(":data:wallets")
include(":data:analytics")
// endregion Data modules