Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-04 16:46:41 +03:00
commit 512ab6d54d
206 changed files with 8933 additions and 630 deletions

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

@ -13,6 +13,10 @@ android {
dependencies {
implementation(deps.androidx.datastore)
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)

View file

@ -1,6 +1,7 @@
package com.tangem.data.card
import com.tangem.TangemSdk
import com.tangem.blockchain.common.CommonSigner
import com.tangem.common.UserCodeType
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.common.core.UserCodeRequestPolicy
@ -57,4 +58,16 @@ internal class DefaultCardSdkConfigRepository(
}
override fun isAccessCodeSavingEnabled(): Boolean = preferencesDataSource.shouldSaveAccessCodes
override fun getCommonSigner(cardId: String?) = CommonSigner(
tangemSdk = sdk,
cardId = cardId,
initialMessage = null,
)
override fun isLinkedTerminal() = sdk.config.linkedTerminal
override fun setLinkedTerminal(isLinked: Boolean?) {
sdk.config.linkedTerminal = isLinked
}
}

View file

@ -1,16 +1,35 @@
package com.tangem.data.settings
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class DefaultSettingsRepository(
private val preferencesDataSource: PreferencesDataSource,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : SettingsRepository {
override suspend fun shouldShowSaveUserWalletScreen(): Boolean {
return withContext(dispatchers.io) { preferencesDataSource.shouldShowSaveUserWalletScreen }
}
override suspend fun isWalletScrollPreviewEnabled(): Boolean {
return appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.WALLETS_SCROLL_PREVIEW_KEY,
default = true,
)
}
override suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) {
appPreferencesStore.store(
key = PreferencesKeys.WALLETS_SCROLL_PREVIEW_KEY,
value = isEnabled,
)
}
}

View file

@ -21,10 +21,12 @@ internal object SettingsDataModule {
@Singleton
fun provideSettingsRepository(
preferencesDataSource: PreferencesDataSource,
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
): SettingsRepository {
return DefaultSettingsRepository(
preferencesDataSource = preferencesDataSource,
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
)
}

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,10 +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.data.tokens.repository.*
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
@ -12,10 +9,7 @@ import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.datasource.local.token.UserMarketCoinsStore
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
@ -93,4 +87,18 @@ internal object TokensDataModule {
): MarketCryptoCurrencyRepository {
return DefaultMarketCryptoCurrencyRepository(userMarketCoinsStore)
}
@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

@ -50,7 +50,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