Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-23 11:16:28 +03:00
commit ca49abf2b1
892 changed files with 17979 additions and 6555 deletions

View file

@ -22,4 +22,7 @@ dependencies {
/** Core shouldn't depend on core, but in case with utils and logging its necessary */
implementation(projects.core.utils)
/** For calculating user id hash */
implementation(tangemDeps.card.core)
}

View file

@ -1,5 +1,7 @@
package com.tangem.core.analytics
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.*
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
@ -19,7 +21,8 @@ interface GlobalAnalyticsEventHandler :
AnalyticsFilterHolder,
ParamsInterceptorHolder,
AnalyticsErrorHandler,
AnalyticsExceptionHandler
AnalyticsExceptionHandler,
AnalyticsUserIdHandler
object Analytics : GlobalAnalyticsEventHandler {
@ -57,6 +60,26 @@ object Analytics : GlobalAnalyticsEventHandler {
return paramsInterceptors.remove(interceptorId)
}
override fun setUserId(userId: String) {
analyticsScope.launch {
val userIdHash = userId.calculateSha256().toHexString()
analyticsMutex.withLock {
analyticsHandlers.filterIsInstance<AnalyticsUserIdHandler>()
.forEach { handler -> handler.setUserId(userIdHash) }
}
}
}
override fun clearUserId() {
analyticsScope.launch {
analyticsMutex.withLock {
analyticsHandlers.filterIsInstance<AnalyticsUserIdHandler>()
.forEach { handler -> handler.clearUserId() }
}
}
}
override fun send(event: AnalyticsEvent) {
analyticsScope.launch {
event.params = applyParamsInterceptors(event)

View file

@ -18,6 +18,11 @@ interface AnalyticsExceptionHandler {
fun sendException(event: ExceptionAnalyticsEvent)
}
interface AnalyticsUserIdHandler {
fun setUserId(userId: String)
fun clearUserId()
}
interface AnalyticsHandler : AnalyticsEventHandler {
fun id(): String

View file

@ -0,0 +1,8 @@
package com.tangem.core.analytics.api
interface UserIdHolder {
fun setUserId(userId: String)
fun clearUserId()
}

View file

@ -51,10 +51,6 @@
"name": "WALLET_CONNECT_REDESIGN_ENABLED",
"version": "undefined"
},
{
"name": "NETWORKS_LOADING_REFACTORING_ENABLED",
"version": "5.23.0"
},
{
"name": "QUOTES_LOADING_REFACTORING_ENABLED",
"version": "5.24.0"
@ -62,5 +58,13 @@
{
"name": "STAKING_LOADING_REFACTORING_ENABLED",
"version": "5.25.0"
},
{
"name": "DEEPLINK_NAVIGATION_ENABLED",
"version": "5.25.0"
},
{
"name": "PUSH_NOTIFICATIONS_ENABLED",
"version": "undefined"
}
]

View file

@ -23,6 +23,7 @@ dependencies {
/** Project */
implementation(projects.core.analytics)
implementation(projects.core.utils)
implementation(projects.core.res)
implementation(projects.libs.auth)
implementation(projects.domain.appTheme.models)
implementation(projects.domain.core)

View file

@ -11,4 +11,9 @@ interface AuthProvider {
fun getCardPublicKey(): String
fun getCardId(): String
/**
* Returns map where keys(cardId) associated with cardPublicKey
*/
fun getCardsPublicKeys(): Map<String, String>
}

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config.managers
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
import kotlinx.coroutines.flow.StateFlow
/**
* Api configs manager
@ -10,8 +11,11 @@ import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
*/
interface ApiConfigsManager {
/** Flag that determines whether the manager is initialized */
val isInitialized: StateFlow<Boolean>
/** Initialize resources */
fun initialize() {}
fun initialize()
/** Get environment config by [id] */
fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig

View file

@ -29,7 +29,13 @@ internal class DevApiConfigsManager(
private val _apiConfigs = MutableStateFlow(value = apiConfigs.associateWith { it.defaultEnvironment })
override val isInitialized: StateFlow<Boolean> get() = _isInitialized.asStateFlow()
private val _isInitialized = MutableStateFlow(value = false)
override fun initialize() {
_isInitialized.value = false
// We can't use appPreferencesStore.getObjectMap as base flow,
// because we should keep possibility to work with configs synchronous.
// See [getBaseUrl]
@ -42,8 +48,12 @@ internal class DevApiConfigsManager(
savedEnvironments[config.id.name] ?: currentEnvironment
}
}
if (!_isInitialized.value) {
_isInitialized.value = true
}
}
.launchIn(CoroutineScope(SupervisorJob() + dispatchers.main))
.launchIn(CoroutineScope(SupervisorJob() + dispatchers.default))
}
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {

View file

@ -3,6 +3,8 @@ package com.tangem.datasource.api.common.config.managers
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* Implementation of [ApiConfigsManager] in PROD environment
@ -13,6 +15,10 @@ internal class ProdApiConfigsManager(
private val apiConfigs: ApiConfigs,
) : ApiConfigsManager {
override val isInitialized: StateFlow<Boolean> = MutableStateFlow(value = true)
override fun initialize() = Unit
override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig {
val config = apiConfigs.firstOrNull { it.id == id }
?: error("Api config with id [$id] not found. Check that ApiConfig with id [$id] was provided into DI")

View file

@ -154,7 +154,7 @@ interface TangemTechApi {
suspend fun updatePushTokenForApplicationId(
@Path("application_id") applicationId: String,
@Body body: NotificationApplicationCreateBody,
): ApiResponse<String>
): ApiResponse<Unit>
@PATCH("user-wallets/wallets/{wallet_id}/notify")
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
@ -172,6 +172,9 @@ interface TangemTechApi {
@GET("user-wallets/wallets/{wallet_id}")
suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse>
@GET("user-wallets/wallets/by-app/{app_id}")
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
// endregion
companion object {

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CardInfoBody(
@Json(name = "card_id") val cardId: String,
@Json(name = "card_public_key") val cardPublicKey: String,
)

View file

@ -9,6 +9,7 @@ data class UserTokensResponse(
@Json(name = "version") val version: Int = 0,
@Json(name = "group") val group: GroupType,
@Json(name = "sort") val sort: SortType,
@Json(name = "notifyStatus") val notifyStatus: Boolean? = null,
@Json(name = "tokens") val tokens: List<Token> = emptyList(),
) {
@ -21,6 +22,7 @@ data class UserTokensResponse(
@Json(name = "symbol") val symbol: String,
@Json(name = "decimals") val decimals: Int,
@Json(name = "contractAddress") val contractAddress: String?,
@Json(name = "addresses") val addresses: List<String>? = null,
) {
override fun equals(other: Any?): Boolean {
val otherToken = other as? Token ?: return false

View file

@ -6,4 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WalletIdBody(
@Json(name = "id") val walletId: String,
@Json(name = "name") val name: String,
@Json(name = "cards") val cards: List<CardInfoBody>,
)

View file

@ -25,16 +25,18 @@ interface TangemVisaApi {
// region: auth
@POST("v1/auth/challenge")
suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): GenerateNonceResponse
suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/challenge")
suspend fun generateNonceByCardWallet(@Body request: GenerateNoneByCardWalletRequest): GenerateNonceResponse
suspend fun generateNonceByCardWallet(
@Body request: GenerateNoneByCardWalletRequest,
): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): JWTResponse
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): JWTResponse
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token/refresh")
suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse<JWTResponse>

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class VisaErrorResponse(
@Json(name = "error") val error: Error,
) {
@JsonClass(generateAdapter = true)
data class Error(
@Json(name = "code") val code: Int,
)
}

View file

@ -43,6 +43,10 @@ internal object NetworkModule {
private const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L
private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L
private val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
// ApiConfig.ID.StakeKit,
)
@Provides
@Singleton
fun provideApiConfigManager(
@ -317,7 +321,7 @@ internal object NetworkModule {
}
b
}
.addLoggers(context)
.addLoggers(context = context, id = id)
.clientBuilder()
.build(),
)
@ -325,6 +329,12 @@ internal object NetworkModule {
.create(T::class.java)
}
private fun OkHttpClient.Builder.addLoggers(context: Context, id: ApiConfig.ID): OkHttpClient.Builder {
if (id in excludedApiForLogging) return this
return addLoggers(context)
}
private data class Timeouts(
val callTimeoutSeconds: Long? = null,
val connectTimeoutSeconds: Long? = null,

View file

@ -1,57 +0,0 @@
package com.tangem.datasource.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.network.DefaultNetworksStatusesStore
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.datasource.utils.setTypes
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object NetworksStatusesStoreModule {
@Singleton
@Provides
fun providePersistenceNetworksStatusesStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): DataStore<Map<String, Set<NetworkStatusDM>>> {
return DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithStringKeyTypes(valueTypes = setTypes<NetworkStatusDM>()),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "networks_statuses") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
}
@Singleton
@Provides
fun provideNetworksStatusesStore(
persistenceNetworksStatusesStore: DataStore<Map<String, Set<NetworkStatusDM>>>,
): NetworksStatusesStore {
return DefaultNetworksStatusesStore(
runtimeDataStore = RuntimeDataStore(),
persistenceDataStore = persistenceNetworksStatusesStore,
)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di.local
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.DefaultUserTokensResponseStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object LocalTokenModule {
@Provides
@Singleton
fun provideUserTokensResponseStore(appPreferencesStore: AppPreferencesStore): UserTokensResponseStore {
return DefaultUserTokensResponseStore(appPreferencesStore = appPreferencesStore)
}
}

View file

@ -43,6 +43,7 @@ internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel,
bittensorOnfinalityApiKey = value.bittensorOnfinalityKey,
koinosProApiKey = value.koinosProApiKey,
alephiumApiKey = value.alephiumTangemApiKey,
moralisApiKey = value.moralisApiKey,
)
}

View file

@ -1,201 +0,0 @@
package com.tangem.datasource.local.network
import androidx.datastore.core.DataStore
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.network.converter.NetworkDerivationPathConverter
import com.tangem.datasource.local.network.converter.NetworkStatusConverter
import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
private typealias NetworkStatusesByWalletId = Map<String, Set<NetworkStatusDM>>
internal class DefaultNetworksStatusesStore(
private val runtimeDataStore: RuntimeDataStore<Set<NetworkStatus>>,
private val persistenceDataStore: DataStore<NetworkStatusesByWalletId>,
) : NetworksStatusesStore {
private val mutex = Mutex()
override fun get(key: UserWalletId): Flow<Set<NetworkStatus>> {
return runtimeDataStore.get(provideStringKey(key))
}
override fun get(key: UserWalletId, networks: Set<Network>): Flow<Set<NetworkStatus>> = channelFlow {
val cachedStatuses = persistenceDataStore.data.firstOrNull()
?.get(key.stringValue)
?.mapNotNullTo(mutableSetOf()) { cached ->
val network = networks.firstOrNull {
it.id == cached.networkId &&
it.derivationPath == NetworkDerivationPathConverter.convert(cached.derivationPath)
}
?: return@mapNotNullTo null
NetworkStatusConverter(network = network, isCached = true).convert(value = cached)
}
.orEmpty()
if (cachedStatuses.isNotEmpty()) {
send(cachedStatuses)
}
runtimeDataStore.get(provideStringKey(key))
.onEach { runtimeStatuses ->
val mergedStatuses = mergeStatuses(
networks = networks,
cachedStatuses = cachedStatuses,
runtimeStatuses = runtimeStatuses,
)
send(mergedStatuses)
}
.launchIn(scope = this)
}
override suspend fun getSyncOrNull(key: UserWalletId): Set<NetworkStatus>? {
val runtimeStatuses = runtimeDataStore.getSyncOrNull(key = provideStringKey(key)) ?: return null
val networks = runtimeStatuses.map(NetworkStatus::network).toSet()
val cachedStatuses = persistenceDataStore.data.firstOrNull()
?.get(key.stringValue)
?.mapNotNullTo(mutableSetOf()) { cached ->
val network = networks.firstOrNull {
it.id == cached.networkId &&
it.derivationPath == NetworkDerivationPathConverter.convert(cached.derivationPath)
}
?: return@mapNotNullTo null
NetworkStatusConverter(network = network, isCached = true).convert(value = cached)
}
.orEmpty()
return mergeStatuses(
networks = networks,
cachedStatuses = cachedStatuses,
runtimeStatuses = runtimeStatuses,
)
}
override suspend fun store(key: UserWalletId, value: NetworkStatus) {
storeAll(key = key, values = setOf(value))
}
override suspend fun storeAll(key: UserWalletId, values: Set<NetworkStatus>) {
mutex.withLock {
coroutineScope {
launch { storeInRuntimeStore(key = key, statuses = values) }
launch { storeInPersistenceStore(userWalletId = key, statuses = values) }
}
}
}
override suspend fun refresh(key: UserWalletId, networks: Set<Network>) {
mutex.withLock {
val currentStatuses = getSyncOrNull(key).orEmpty()
storeInRuntimeStore(
key = key,
statuses = networks.mapNotNullTo(hashSetOf()) { network ->
val status = currentStatuses.firstOrNull {
it.network.id == network.id && it.network.derivationPath == network.derivationPath
} ?: return@mapNotNullTo null
status.copy(
value = status.value.copySealed(source = StatusSource.CACHE),
)
},
)
}
}
/**
* Merge [cachedStatuses] with [runtimeStatuses]
* The resulting set contains statuses from both sets.
* If a status with the same network is in both sets, the status from [runtimeStatuses] is used.
*/
private fun mergeStatuses(
networks: Set<Network>,
cachedStatuses: Set<NetworkStatus>,
runtimeStatuses: Set<NetworkStatus>,
): Set<NetworkStatus> {
return networks.mapNotNullTo(hashSetOf()) { network ->
val runtimeStatus = runtimeStatuses.firstOrNull { it.network == network }
if (runtimeStatus == null) {
getCachedStatusIfPossible(
cachedStatuses = cachedStatuses,
network = network,
source = StatusSource.CACHE,
)
} else if (runtimeStatus.value is NetworkStatus.Unreachable) {
getCachedStatusIfPossible(
cachedStatuses = cachedStatuses,
network = network,
source = StatusSource.ONLY_CACHE,
)
?: runtimeStatus
} else {
runtimeStatus
}
}
}
private fun getCachedStatusIfPossible(
cachedStatuses: Set<NetworkStatus>,
network: Network,
source: StatusSource,
): NetworkStatus? {
val cached = cachedStatuses.firstOrNull { it.network == network } ?: return null
val updatedCachedStatus = when (val status = cached.value) {
is NetworkStatus.NoAccount -> status.copy(source = source)
is NetworkStatus.Verified -> status.copy(source = source)
is NetworkStatus.Unreachable,
is NetworkStatus.MissedDerivation,
-> null
}
return if (updatedCachedStatus != null) {
cached.copy(value = updatedCachedStatus)
} else {
null
}
}
private suspend fun storeInRuntimeStore(key: UserWalletId, statuses: Set<NetworkStatus>) {
val updatedValues = getSyncOrNull(key).orEmpty()
.addOrReplace(items = statuses) { prev, new -> prev.network == new.network }
runtimeDataStore.store(key = provideStringKey(key), value = updatedValues)
}
private suspend fun storeInPersistenceStore(userWalletId: UserWalletId, statuses: Set<NetworkStatus>) {
// Converter will return null if the network status is not supported
val newStatuses = NetworkStatusDataModelConverter.convertSet(input = statuses).filterNotNull().toSet()
persistenceDataStore.updateData { storedStatuses ->
storedStatuses.toMutableMap().apply {
val updatedValues = this[userWalletId.stringValue].orEmpty()
.addOrReplace(newStatuses) { prev, new ->
prev.networkId == new.networkId && prev.derivationPath == new.derivationPath
}
this[userWalletId.stringValue] = updatedValues
}
}
}
private fun provideStringKey(key: UserWalletId): String {
return "network_statuses_${key.stringValue}"
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.datasource.local.network
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
interface NetworksStatusesStore {
fun get(key: UserWalletId): Flow<Set<NetworkStatus>>
fun get(key: UserWalletId, networks: Set<Network>): Flow<Set<NetworkStatus>>
suspend fun getSyncOrNull(key: UserWalletId): Set<NetworkStatus>?
suspend fun store(key: UserWalletId, value: NetworkStatus)
suspend fun storeAll(key: UserWalletId, values: Set<NetworkStatus>)
suspend fun refresh(key: UserWalletId, networks: Set<Network>)
}

View file

@ -1,60 +0,0 @@
package com.tangem.datasource.local.network.converter
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.utils.converter.TwoWayConverter
import timber.log.Timber
/**
* Converter from [Set<NetworkStatusDM.Address>] to [NetworkAddress] and vice versa
*
[REDACTED_AUTHOR]
*/
class NetworkAddressConverter(
private val selectedAddress: String,
) : TwoWayConverter<Set<NetworkStatusDM.Address>, NetworkAddress> {
override fun convert(value: Set<NetworkStatusDM.Address>): NetworkAddress {
val defaultAddress = value
.firstOrNull { it.value == selectedAddress }
?.let(::toNetworkAddress)
requireNotNull(defaultAddress) { "Selected address must not be null" }
return if (value.size != 1) {
NetworkAddress.Selectable(
defaultAddress = defaultAddress,
availableAddresses = value.mapTo(destination = hashSetOf(), transform = ::toNetworkAddress),
)
} else {
NetworkAddress.Single(defaultAddress = defaultAddress)
}
}
override fun convertBack(value: NetworkAddress): Set<NetworkStatusDM.Address> {
return value.availableAddresses
.map { address ->
NetworkStatusDM.Address(
value = address.value,
type = when (address.type) {
NetworkAddress.Address.Type.Primary -> NetworkStatusDM.Address.Type.Primary
NetworkAddress.Address.Type.Secondary -> NetworkStatusDM.Address.Type.Secondary
},
)
}
.toSet()
}
private fun toNetworkAddress(address: NetworkStatusDM.Address): NetworkAddress.Address {
val type = when (address.type) {
NetworkStatusDM.Address.Type.Primary -> NetworkAddress.Address.Type.Primary
NetworkStatusDM.Address.Type.Secondary -> NetworkAddress.Address.Type.Secondary
}
if (address.value.isBlank()) {
Timber.w("Address value is blank")
}
return NetworkAddress.Address(value = address.value, type = type)
}
}

View file

@ -1,35 +0,0 @@
package com.tangem.datasource.local.network.converter
import com.tangem.common.extensions.mapNotNullValues
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus
import com.tangem.utils.converter.TwoWayConverter
import java.math.BigDecimal
private typealias AmountsDataModel = Map<String, BigDecimal>
private typealias AmountsDomainModel = Map<CryptoCurrency.ID, CryptoCurrencyAmountStatus>
/**
* Converter from [AmountsDataModel] to [AmountsDomainModel] and vice versa
*
[REDACTED_AUTHOR]
*/
object NetworkAmountsConverter : TwoWayConverter<AmountsDataModel, AmountsDomainModel> {
override fun convert(value: AmountsDataModel): AmountsDomainModel {
return value
.mapKeys { CryptoCurrency.ID.fromValue(value = it.key) }
.mapValues { (_, amount) -> CryptoCurrencyAmountStatus.Loaded(value = amount) }
}
override fun convertBack(value: AmountsDomainModel): AmountsDataModel {
return value
.mapKeys { (id, _) -> id.value }
.mapNotNullValues { (_, amount) ->
when (amount) {
is CryptoCurrencyAmountStatus.Loaded -> amount.value
is CryptoCurrencyAmountStatus.NotFound -> null
}
}
}
}

View file

@ -1,34 +0,0 @@
package com.tangem.datasource.local.network.converter
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.datasource.local.network.entity.NetworkStatusDM.DerivationPath.Type
import com.tangem.domain.tokens.model.Network
import com.tangem.utils.converter.TwoWayConverter
/**
* Converter from [NetworkStatusDM.DerivationPath] to [Network.DerivationPath] and vice versa
*
[REDACTED_AUTHOR]
*/
object NetworkDerivationPathConverter :
TwoWayConverter<NetworkStatusDM.DerivationPath, Network.DerivationPath> {
override fun convert(value: NetworkStatusDM.DerivationPath): Network.DerivationPath {
return when (value.type) {
Type.CARD -> Network.DerivationPath.Card(value.value)
Type.CUSTOM -> Network.DerivationPath.Custom(value.value)
Type.NONE -> Network.DerivationPath.None
}
}
override fun convertBack(value: Network.DerivationPath): NetworkStatusDM.DerivationPath {
return NetworkStatusDM.DerivationPath(
value = value.value.orEmpty(),
type = when (value) {
is Network.DerivationPath.Card -> Type.CARD
is Network.DerivationPath.Custom -> Type.CUSTOM
Network.DerivationPath.None -> Type.NONE
},
)
}
}

View file

@ -1,47 +0,0 @@
package com.tangem.datasource.local.network.converter
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.utils.converter.Converter
/**
* Converter from [NetworkStatusDM] to [NetworkStatus]
*
* @property network network
* @property isCached flag that determines whether the status is a cache
*
[REDACTED_AUTHOR]
*/
internal class NetworkStatusConverter(
private val network: Network,
private val isCached: Boolean,
) : Converter<NetworkStatusDM, NetworkStatus> {
override fun convert(value: NetworkStatusDM): NetworkStatus {
val address = NetworkAddressConverter(selectedAddress = value.selectedAddress)
.convert(value = value.availableAddresses)
val status = when (value) {
is NetworkStatusDM.Verified -> {
NetworkStatus.Verified(
address = address,
amounts = NetworkAmountsConverter.convert(value = value.amounts),
pendingTransactions = mapOf(),
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
)
}
is NetworkStatusDM.NoAccount -> {
NetworkStatus.NoAccount(
address = address,
amountToCreateAccount = value.amountToCreateAccount,
errorMessage = value.errorMessage,
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
)
}
}
return NetworkStatus(network = network, value = status)
}
}

View file

@ -1,40 +0,0 @@
package com.tangem.datasource.local.network.converter
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.utils.converter.Converter
/**
* Converter from [NetworkStatus] to [NetworkStatusDM]
*
[REDACTED_AUTHOR]
*/
object NetworkStatusDataModelConverter : Converter<NetworkStatus, NetworkStatusDM?> {
override fun convert(value: NetworkStatus): NetworkStatusDM? {
return when (val status = value.value) {
is NetworkStatus.Verified -> {
NetworkStatusDM.Verified(
networkId = value.network.id,
derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath),
selectedAddress = status.address.defaultAddress.value,
availableAddresses = NetworkAddressConverter(selectedAddress = status.address.defaultAddress.value)
.convertBack(value = status.address),
amounts = NetworkAmountsConverter.convertBack(value = status.amounts),
)
}
is NetworkStatus.NoAccount -> {
NetworkStatusDM.NoAccount(
networkId = value.network.id,
derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath),
selectedAddress = status.address.defaultAddress.value,
availableAddresses = NetworkAddressConverter(selectedAddress = status.address.defaultAddress.value)
.convertBack(value = status.address),
amountToCreateAccount = status.amountToCreateAccount,
errorMessage = status.errorMessage,
)
}
else -> null
}
}
}

View file

@ -2,29 +2,63 @@ package com.tangem.datasource.local.network.entity
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.domain.tokens.model.Network
import com.tangem.datasource.local.network.entity.NetworkStatusDM.NoAccount
import com.tangem.datasource.local.network.entity.NetworkStatusDM.Verified
import dev.onenowy.moshipolymorphicadapter.PolymorphicAdapterType
import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
import java.math.BigDecimal
/**
* Network status for storage in the local cache. Supports two types - the [Verified] and [NoAccount].
*
* @see [com.tangem.domain.tokens.model.NetworkStatus]
*/
@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER)
sealed interface NetworkStatusDM {
val networkId: Network.ID
/** Network id */
val networkId: ID
/** Derivation path */
val derivationPath: DerivationPath
/** Selected address */
val selectedAddress: String
/** Available address */
val availableAddresses: Set<Address>
/**
* Verified
*
* @property networkId network id
* @property derivationPath derivation path
* @property selectedAddress selected address
* @property availableAddresses available addresses
* @property amounts amounts
*/
@NameLabel("amounts")
data class Verified(
@Json(name = "network_id") override val networkId: Network.ID,
@Json(name = "network_id") override val networkId: ID,
@Json(name = "derivation_path") override val derivationPath: DerivationPath,
@Json(name = "selected_address") override val selectedAddress: String,
@Json(name = "available_addresses") override val availableAddresses: Set<Address>,
@Json(name = "amounts") val amounts: Map<String, BigDecimal>,
) : NetworkStatusDM
/**
* No account
*
* @property networkId network id
* @property derivationPath derivation path
* @property selectedAddress selected address
* @property availableAddresses available addresses
* @property amountToCreateAccount amount to create account
* @property errorMessage error message
*/
@NameLabel("amount_to_create_account")
data class NoAccount(
@Json(name = "network_id") override val networkId: Network.ID,
@Json(name = "network_id") override val networkId: ID,
@Json(name = "derivation_path") override val derivationPath: DerivationPath,
@Json(name = "selected_address") override val selectedAddress: String,
@Json(name = "available_addresses") override val availableAddresses: Set<Address>,
@ -32,6 +66,11 @@ sealed interface NetworkStatusDM {
@Json(name = "error_message") val errorMessage: String,
) : NetworkStatusDM
@JsonClass(generateAdapter = true)
data class ID(
@Json(name = "value") val value: String,
)
@JsonClass(generateAdapter = true)
data class DerivationPath(
@Json(name = "value") val value: String,

View file

@ -3,13 +3,14 @@ package com.tangem.datasource.local.nft
import androidx.datastore.core.DataStore
import com.tangem.blockchain.nft.models.NFTAsset
import com.tangem.blockchain.nft.models.NFTCollection
import com.tangem.datasource.local.nft.custom.NFTPriceId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
internal class DefaultNFTPersistenceStore(
private val collectionsPersistenceStore: DataStore<List<NFTCollection>>,
private val pricesPersistenceStore: DataStore<Map<NFTAsset.Identifier, NFTAsset.SalePrice>>,
private val pricesPersistenceStore: DataStore<List<NFTPriceId>>,
) : NFTPersistenceStore {
override fun getCollections(): Flow<List<NFTCollection>?> = collectionsPersistenceStore.data
@ -27,11 +28,12 @@ internal class DefaultNFTPersistenceStore(
}
override fun getSalePrice(assetId: NFTAsset.Identifier): Flow<NFTAsset.SalePrice?> = pricesPersistenceStore.data
.map { it[assetId] }
.map { data -> data.associate { it.assetId to it.price }[assetId] }
override suspend fun getSalePricesSync(): Map<NFTAsset.Identifier, NFTAsset.SalePrice>? = pricesPersistenceStore
.data
.firstOrNull()
?.associate { it.assetId to it.price }
override suspend fun saveCollections(collections: List<NFTCollection>) {
collectionsPersistenceStore.updateData {
@ -41,9 +43,14 @@ internal class DefaultNFTPersistenceStore(
override suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice) {
pricesPersistenceStore.updateData {
it.toMutableMap().apply { this[assetId] = salePrice }
it.toMutableList() + NFTPriceId(assetId = assetId, price = salePrice)
}
}
override suspend fun clear() {
collectionsPersistenceStore.updateData { emptyList() }
pricesPersistenceStore.updateData { emptyList() }
}
private fun NFTCollection.getAsset(assetId: NFTAsset.Identifier) = assets.firstOrNull { it.identifier == assetId }
}

View file

@ -2,11 +2,11 @@ package com.tangem.datasource.local.nft
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.network.Network
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.nft.models.NFTCollections
import com.tangem.domain.nft.models.NFTSalePrice
import com.tangem.domain.tokens.model.Network
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
@ -71,6 +71,16 @@ internal class DefaultNFTRuntimeStore(
}
}
override suspend fun clear() {
collectionsRuntimeStore.store(
NFTCollections(
network = network,
content = NFTCollections.Content.Collections(null, StatusSource.ONLY_CACHE),
),
)
pricesRuntimeStore.store(emptyMap())
}
private fun NFTCollections.getCollection(collectionId: NFTCollection.Identifier): NFTCollection? =
(content as? NFTCollections.Content.Collections)
?.collections
@ -105,7 +115,6 @@ internal class DefaultNFTRuntimeStore(
-> assets
is NFTCollection.Assets.Value -> assets.copy(
items = assets.items
.filter { !it.name.isNullOrEmpty() }
.sortedBy { it.name }
.map { asset ->
asset.mergeWithPrice(prices[asset.id] ?: NFTSalePrice.Empty(asset.id))
@ -122,7 +131,7 @@ internal class DefaultNFTRuntimeStore(
)
}
?.filter { it.count > 0 }
?.sortedBy { it.name },
?.sortedBy { it.name?.lowercase() },
source = this.source,
)

View file

@ -18,4 +18,6 @@ interface NFTPersistenceStore {
suspend fun saveCollections(collections: List<NFTCollection>)
suspend fun saveSalePrice(assetId: NFTAsset.Identifier, salePrice: NFTAsset.SalePrice)
suspend fun clear()
}

View file

@ -5,13 +5,12 @@ import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.blockchain.nft.models.NFTAsset
import com.tangem.blockchain.nft.models.NFTCollection
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.nft.custom.NFTPriceId
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes
import com.tangem.datasource.utils.mapWithCustomKeyTypes
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext
@ -48,8 +47,8 @@ class NFTPersistenceStoreFactory @Inject constructor(
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_prices
// result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_prices
fileName = "nft_${userWalletStringId}_${networkStringId}_prices",
types = mapWithCustomKeyTypes<NFTAsset.Identifier, NFTAsset.SalePrice>(),
defaultValue = emptyMap(),
types = listTypes<NFTPriceId>(),
defaultValue = emptyList(),
),
)
}
@ -65,7 +64,7 @@ class NFTPersistenceStoreFactory @Inject constructor(
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
)
private fun Network.ID.formatted(): String = value
private fun Network.ID.formatted(): String = rawId.value
.filter(Char::isLetterOrDigit)
.lowercase()

View file

@ -23,4 +23,6 @@ interface NFTRuntimeStore {
suspend fun saveCollections(collections: NFTCollections)
suspend fun saveSalePrice(salePrice: NFTSalePrice)
suspend fun clear()
}

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.local.nft
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.network.Network
import javax.inject.Inject
import javax.inject.Singleton

View file

@ -1,9 +1,9 @@
package com.tangem.datasource.local.nft.converter
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.network.Network
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTSalePrice
import com.tangem.domain.tokens.model.Network
import com.tangem.utils.converter.TwoWayConverter
import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset
@ -57,7 +57,7 @@ object NFTSdkAssetConverter : TwoWayConverter<Pair<Network, SdkNFTAsset>, NFTAss
return value.network to SdkNFTAsset(
identifier = assetId,
collectionIdentifier = collectionId,
blockchainId = value.network.id.value,
blockchainId = value.network.rawId,
contractType = value.contractType,
owner = value.owner,
name = value.name,

View file

@ -5,14 +5,16 @@ import com.tangem.domain.nft.models.NFTSalePrice
import com.tangem.utils.converter.TwoWayConverter
import com.tangem.blockchain.nft.models.NFTAsset.SalePrice as SDKSalePrice
internal class NFTSdkAssetSalePriceConverter(
class NFTSdkAssetSalePriceConverter(
private val assetId: NFTAsset.Identifier,
) : TwoWayConverter<SDKSalePrice, NFTSalePrice.Value> {
override fun convert(value: SDKSalePrice): NFTSalePrice.Value {
return NFTSalePrice.Value(
assetId = assetId,
fiatValue = null,
value = value.value,
symbol = value.symbol,
symbol = value.symbol.orEmpty(),
decimals = value.decimals ?: 0,
)
}
@ -20,6 +22,7 @@ internal class NFTSdkAssetSalePriceConverter(
return SDKSalePrice(
symbol = value.symbol,
value = value.value,
decimals = value.decimals,
)
}
}

View file

@ -1,20 +1,26 @@
package com.tangem.datasource.local.nft.converter
import android.content.res.Resources
import com.tangem.datasource.R
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.network.Network
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.domain.tokens.model.Network
import com.tangem.utils.converter.Converter
import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection
object NFTSdkCollectionConverter : Converter<Pair<Network, SdkNFTCollection>, NFTCollection> {
class NFTSdkCollectionConverter(
private val resources: Resources,
) : Converter<Pair<Network, SdkNFTCollection>, NFTCollection> {
override fun convert(value: Pair<Network, SdkNFTCollection>): NFTCollection {
val (network, collection) = value
val collectionId = NFTSdkCollectionIdentifierConverter.convert(collection.identifier)
return NFTCollection(
id = collectionId,
network = network,
name = collection.name,
// We use localised strings from resources here to proceed sorting and searching correctly
// Sorting is invoked on data layer, searching is simplified to filtering for now and invoked in Model
name = collection.toName(collectionId),
description = collection.description,
logoUrl = collection.logoUrl,
count = collection.count,
@ -37,4 +43,26 @@ object NFTSdkCollectionConverter : Converter<Pair<Network, SdkNFTCollection>, NF
},
)
}
private fun SdkNFTCollection.toName(collectionId: NFTCollection.Identifier) = when (collectionId) {
is NFTCollection.Identifier.EVM ->
name.toCollectionName()
is NFTCollection.Identifier.TON -> if (collectionId.contractAddress == null) {
resources.getString(R.string.nft_no_collection)
} else {
name.toCollectionName()
}
is NFTCollection.Identifier.Solana -> if (collectionId.collectionAddress == null) {
resources.getString(R.string.nft_no_collection)
} else {
name.toCollectionName()
}
NFTCollection.Identifier.Unknown -> null
}
private fun String?.toCollectionName() = if (this.isNullOrEmpty()) {
resources.getString(R.string.nft_untitled_collection)
} else {
this
}
}

View file

@ -13,7 +13,7 @@ object NFTSdkCollectionIdentifierConverter : TwoWayConverter<SdkNFTCollection.Id
contractAddress = value.contractAddress,
)
is SdkNFTCollection.Identifier.Solana -> NFTCollection.Identifier.Solana(
collection = value.collection,
collectionAddress = value.collectionAddress,
)
is SdkNFTCollection.Identifier.Unknown -> NFTCollection.Identifier.Unknown
}
@ -26,7 +26,7 @@ object NFTSdkCollectionIdentifierConverter : TwoWayConverter<SdkNFTCollection.Id
contractAddress = value.contractAddress,
)
is NFTCollection.Identifier.Solana -> SdkNFTCollection.Identifier.Solana(
collection = value.collection,
collectionAddress = value.collectionAddress,
)
is NFTCollection.Identifier.Unknown -> SdkNFTCollection.Identifier.Unknown
}

View file

@ -0,0 +1,8 @@
package com.tangem.datasource.local.nft.custom
import com.tangem.blockchain.nft.models.NFTAsset
data class NFTPriceId(
val assetId: NFTAsset.Identifier,
val price: NFTAsset.SalePrice,
)

View file

@ -0,0 +1,8 @@
package com.tangem.datasource.local.nft.custom
import com.tangem.blockchain.nft.models.NFTAsset
data class NFTPriceKeyValue(
val key: NFTAsset.Identifier,
val value: NFTAsset.SalePrice,
)

View file

@ -5,7 +5,7 @@ import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.quote.converter.QuoteConverter
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.coroutineScope

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.local.quote
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import kotlinx.coroutines.flow.Flow

View file

@ -2,7 +2,7 @@ package com.tangem.datasource.local.quote.converter
import com.tangem.datasource.api.tangemTech.models.QuotesResponse
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero

View file

@ -1,8 +1,8 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.domain.wallets.models.UserWalletId
/**
* Default implementation of [UserTokensResponseStore]
*
* @property appPreferencesStore app preferences store
*
[REDACTED_AUTHOR]
*/
internal class DefaultUserTokensResponseStore(
private val appPreferencesStore: AppPreferencesStore,
) : UserTokensResponseStore {
override suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse? {
return appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue),
)
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.local.token
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.wallets.models.UserWalletId
/**
* Store of [UserTokensResponse]
*
[REDACTED_AUTHOR]
*/
interface UserTokensResponseStore {
/** Get [UserTokensResponse] synchronously by [userWalletId] or null */
suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse?
}

View file

@ -2,25 +2,25 @@ package com.tangem.datasource.local.txhistory
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.txhistory.models.Page
import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.utils.extensions.addOrReplace
internal class DefaultTxHistoryItemsStore(
dataStore: StringKeyDataStore<Set<PaginationWrapper<TxHistoryItem>>>,
dataStore: StringKeyDataStore<Set<PaginationWrapper<TxInfo>>>,
) : TxHistoryItemsStore,
StringKeyDataStoreDecorator<TxHistoryItemsStore.Key, Set<PaginationWrapper<TxHistoryItem>>>(dataStore) {
StringKeyDataStoreDecorator<TxHistoryItemsStore.Key, Set<PaginationWrapper<TxInfo>>>(dataStore) {
override fun provideStringKey(key: TxHistoryItemsStore.Key): String = key.toString()
override suspend fun getSyncOrNull(key: TxHistoryItemsStore.Key, page: Page): PaginationWrapper<TxHistoryItem>? {
override suspend fun getSyncOrNull(key: TxHistoryItemsStore.Key, page: Page): PaginationWrapper<TxInfo>? {
val storedValue = getSyncOrNull(key)
return storedValue?.firstOrNull { it.currentPage == page }
}
override suspend fun store(key: TxHistoryItemsStore.Key, value: PaginationWrapper<TxHistoryItem>) {
override suspend fun store(key: TxHistoryItemsStore.Key, value: PaginationWrapper<TxInfo>) {
val oldValue = getSyncOrNull(key).orEmpty()
val newValue = oldValue.addOrReplace(value) {
it.currentPage == value.currentPage

View file

@ -1,18 +1,18 @@
package com.tangem.datasource.local.txhistory
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.TxInfo
import com.tangem.domain.txhistory.models.Page
import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.wallets.models.UserWalletId
interface TxHistoryItemsStore {
suspend fun getSyncOrNull(key: Key, page: Page): PaginationWrapper<TxHistoryItem>?
suspend fun getSyncOrNull(key: Key, page: Page): PaginationWrapper<TxInfo>?
suspend fun remove(key: Key)
suspend fun store(key: Key, value: PaginationWrapper<TxHistoryItem>)
suspend fun store(key: Key, value: PaginationWrapper<TxInfo>)
data class Key(
val userWalletId: UserWalletId,

View file

@ -0,0 +1,195 @@
package com.tangem.datasource.local.network.entity
import com.google.common.truth.Truth
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.datasource.api.common.adapter.BigDecimalAdapter
import dev.onenowy.moshipolymorphicadapter.NamePolymorphicAdapterFactory
import org.junit.Test
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class NetworkStatusDMSerializationTest {
private val moshi = Moshi.Builder()
.add(
NamePolymorphicAdapterFactory.of(NetworkStatusDM::class.java)
.withSubtype(NetworkStatusDM.Verified::class.java, "amounts")
.withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"),
)
.add(BigDecimalAdapter())
.addLast(KotlinJsonAdapterFactory())
.build()
@OptIn(ExperimentalStdlibApi::class)
private val adapter = moshi.adapter<NetworkStatusDM>()
@Test
fun `deserialize JSON to Verified`() {
// Arrange
val json = """
{
"network_id": { "value": "ETH" },
"derivation_path": {
"value": "m/44'/60'/0'/0/0",
"type": "card"
},
"selected_address": "0x123456",
"available_addresses": [
{ "value": "0x123456", "type": "primary" },
{ "value": "0xabcdef", "type": "secondary" }
],
"amounts": { "ETH": "1.2345" }
}
""".trimIndent()
// Act
val result = adapter.fromJson(json)
// Assert
val expected = NetworkStatusDM.Verified(
networkId = NetworkStatusDM.ID("ETH"),
derivationPath = NetworkStatusDM.DerivationPath(
value = "m/44'/60'/0'/0/0",
type = NetworkStatusDM.DerivationPath.Type.CARD,
),
selectedAddress = "0x123456",
availableAddresses = setOf(
NetworkStatusDM.Address("0x123456", NetworkStatusDM.Address.Type.Primary),
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
),
amounts = mapOf("ETH" to BigDecimal("1.2345")),
)
Truth.assertThat(result).isEqualTo(expected)
}
@Test
fun `serialize Verified to JSON`() {
// Arrange
val model = NetworkStatusDM.Verified(
networkId = NetworkStatusDM.ID("ETH"),
derivationPath = NetworkStatusDM.DerivationPath(
value = "m/44'/60'/0'/0/0",
type = NetworkStatusDM.DerivationPath.Type.CARD,
),
selectedAddress = "0x123456",
availableAddresses = setOf(
NetworkStatusDM.Address("0x123456", NetworkStatusDM.Address.Type.Primary),
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
),
amounts = mapOf("ETH" to BigDecimal("1.2345")),
)
// Act
val actual = adapter.toJson(model)
// Assert
val expected = """
{
"network_id": { "value": "ETH" },
"derivation_path": {
"value": "m/44'/60'/0'/0/0",
"type": "card"
},
"selected_address": "0x123456",
"available_addresses": [
{ "value": "0x123456", "type": "primary" },
{ "value": "0xabcdef", "type": "secondary" }
],
"amounts": { "ETH": "1.2345" }
}
""".stripJsonWhitespace()
Truth.assertThat(actual).isEqualTo(expected)
}
@Test
fun `deserialize JSON to NoAccount`() {
// Arrange
val json = """
{
"network_id": { "value": "ETH" },
"derivation_path": {
"value": "m/44'/60'/0'/0/0",
"type": "card"
},
"selected_address": "0x123456",
"available_addresses": [
{ "value": "0x123456", "type": "primary" },
{ "value": "0xabcdef", "type": "secondary" }
],
"amount_to_create_account": "0.05",
"error_message": "Account not found"
}
""".trimIndent()
// Act
val result = adapter.fromJson(json)
// Assert
val expected = NetworkStatusDM.NoAccount(
networkId = NetworkStatusDM.ID("ETH"),
derivationPath = NetworkStatusDM.DerivationPath(
value = "m/44'/60'/0'/0/0",
type = NetworkStatusDM.DerivationPath.Type.CARD,
),
selectedAddress = "0x123456",
availableAddresses = setOf(
NetworkStatusDM.Address("0x123456", NetworkStatusDM.Address.Type.Primary),
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
),
amountToCreateAccount = BigDecimal("0.05"),
errorMessage = "Account not found",
)
Truth.assertThat(result).isEqualTo(expected)
}
@Test
fun `serialize NoAccount to JSON`() {
// Arrange
val model = NetworkStatusDM.NoAccount(
networkId = NetworkStatusDM.ID("ETH"),
derivationPath = NetworkStatusDM.DerivationPath(
value = "m/44'/60'/0'/0/0",
type = NetworkStatusDM.DerivationPath.Type.CARD,
),
selectedAddress = "0x123456",
availableAddresses = setOf(
NetworkStatusDM.Address("0x123456", NetworkStatusDM.Address.Type.Primary),
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
),
amountToCreateAccount = BigDecimal("0.05"),
errorMessage = "error",
)
// Act
val actual = adapter.toJson(model)
// Assert
val expected = """
{
"network_id": { "value": "ETH" },
"derivation_path": {
"value": "m/44'/60'/0'/0/0",
"type": "card"
},
"selected_address": "0x123456",
"available_addresses": [
{ "value": "0x123456", "type": "primary" },
{ "value": "0xabcdef", "type": "secondary" }
],
"amount_to_create_account": "0.05",
"error_message": "error"
}
""".stripJsonWhitespace()
Truth.assertThat(actual).isEqualTo(expected)
}
private fun String.stripJsonWhitespace(): String = replace(regex = "\\s".toRegex(), replacement = "")
}

View file

@ -2,6 +2,7 @@ package com.tangem.core.deeplink.global
import com.tangem.core.deeplink.DeepLink
@Deprecated("Use BuyDeepLinkHandler")
class BuyCurrencyDeepLink(
val onReceive: () -> Unit,
) : DeepLink() {

View file

@ -2,6 +2,7 @@ package com.tangem.core.deeplink.global
import com.tangem.core.deeplink.DeepLink
@Deprecated("Use ReferralDeepLinkHandler")
class ReferralDeepLink(
val onReceive: () -> Unit,
) : DeepLink(shouldHandleDelayed = true) {

View file

@ -2,6 +2,7 @@ package com.tangem.core.deeplink.global
import com.tangem.core.deeplink.DeepLink
@Deprecated("Use SellDeepLinkHandler")
class SellCurrencyDeepLink(
val onReceive: (data: Data) -> Unit,
shouldHandleDelayed: Boolean,

View file

@ -195,6 +195,8 @@
<string name="common_server_unavailable">Der Server ist nicht verfügbar. Bitte versuche es später erneut.</string>
<string name="common_share">Teilen</string>
<string name="common_share_link">Link teilen</string>
<string name="common_show_less">Weniger anzeigen</string>
<string name="common_show_more">Mehr anzeigen</string>
<string name="common_sign">Signieren</string>
<string name="common_sign_and_send">Signieren und senden</string>
<string name="common_stake">Staken</string>
@ -278,7 +280,7 @@
<string name="exchange_tokens_unavailable_tokens_header">Kann nicht gegen %s ausgetauscht werden</string>
<string name="express_by_provider">Bereitgestellt von</string>
<string name="express_cex_status_button_title">Status</string>
<string name="express_choose_providers_subtitle">Tangem bietet Token-Swaps über Drittanbieter gemäß den jeweiligen Bedingungen des jeweiligen Anbieters an.</string>
<string name="express_choose_providers_subtitle">Anbieter erleichtern Transaktionen</string>
<string name="express_choose_providers_title">Anbieter</string>
<string name="express_error_code">Es ist ein Fehler aufgetreten. Code: %s</string>
<string name="express_error_provider_amount_roundup">Fehler %1$s. Der ausgewählte Anbieter kann die angegebene Transaktion nicht verarbeiten. Bitte runde den Wert auf %2$s auf oder änder diesen.</string>
@ -297,33 +299,33 @@
<string name="express_exchange_status_bought">Gekauft</string>
<string name="express_exchange_status_buying">Einkaufen</string>
<string name="express_exchange_status_buying_active">Einkaufen...</string>
<string name="express_exchange_status_canceled">Abgebrochen</string>
<string name="express_exchange_status_confirmed">Bestätigt</string>
<string name="express_exchange_status_confirming">Bestätigen</string>
<string name="express_exchange_status_confirming_active">Wird bestätigt...</string>
<string name="express_exchange_status_exchanged">Getauscht</string>
<string name="express_exchange_status_exchanging">Tauschen läuft</string>
<string name="express_exchange_status_exchanging_active">Austauschen...</string>
<string name="express_exchange_status_failed">Fehlgeschlagen</string>
<string name="express_exchange_status_paused">Pausiert</string>
<string name="express_exchange_status_canceled">Transaktion abgebrochen</string>
<string name="express_exchange_status_confirmed">Anzahlung bestätigt</string>
<string name="express_exchange_status_confirming">Warten auf Bestätigung</string>
<string name="express_exchange_status_confirming_active">Warte auf Bestätigung...</string>
<string name="express_exchange_status_exchanged">Umtausch abgeschlossen</string>
<string name="express_exchange_status_exchanging">Warten auf Umtausch</string>
<string name="express_exchange_status_exchanging_active">Warte auf den Wechsel...</string>
<string name="express_exchange_status_failed">Umtausch fehlgeschlagen</string>
<string name="express_exchange_status_paused">Tausch pausiert</string>
<string name="express_exchange_status_received">Einzahlung empfangen</string>
<string name="express_exchange_status_receiving">Einzahlung wird erwartet</string>
<string name="express_exchange_status_receiving">Warten auf Einzahlung</string>
<string name="express_exchange_status_receiving_active">Warten auf Einzahlung...</string>
<string name="express_exchange_status_refunded">Rückerstattet</string>
<string name="express_exchange_status_refunding">Rückerstattung</string>
<string name="express_exchange_status_refunded">Rückerstattung abgeschlossen</string>
<string name="express_exchange_status_refunding">Warten auf Rückerstattung</string>
<string name="express_exchange_status_sending">An dich gesendet</string>
<string name="express_exchange_status_sending_active">wird an dich versendet...</string>
<string name="express_exchange_status_sent">Gesendet</string>
<string name="express_exchange_status_sending_active">Betrag senden...</string>
<string name="express_exchange_status_sent">Gesendete Gelder</string>
<string name="express_exchange_status_subtitle">Daten stammen vom Anbieter. Der geschätzte Betrag kann sich aufgrund der Marktbedingungen ändern.</string>
<string name="express_exchange_status_title">Status des Tausches</string>
<string name="express_exchange_status_verifying">Verifizierung erforderlich</string>
<string name="express_exchange_status_waiting_tx_hash">Wartet auf Transaktions-Hash</string>
<string name="express_exchange_token_list_subtitle">Liste aller Token, die deiner Wallet hinzugefügt wurden</string>
<string name="express_fetch_best_rates">Beste Preise werden abgerufen …</string>
<string name="express_fetch_best_rates">Aktuelle Preise werden abgerufen ...</string>
<string name="express_floating_rate">Variabler Zinssatz</string>
<string name="express_legal_one_placeholder">Durch die Nutzung der Swap-Funktion erklärst du dich mit den folgenden Bedingungen des Anbieters einverstanden %s</string>
<string name="express_legal_two_placeholders">Durch die Nutzung der Swap-Funktionalität erklärst du dich mit des Anbieters %1$s und %2$s einverstanden</string>
<string name="express_more_providers_soon">Weitere Anbieter werden bald folgen.\nBleib dabei!</string>
<string name="express_more_providers_soon">Weitere Anbieter folgen in Kürze</string>
<string name="express_provider">Anbieter</string>
<string name="express_provider_best_rate">Bester Preis</string>
<string name="express_provider_max_amount">Verfügbar bis zu %s</string>
@ -337,6 +339,7 @@
<string name="express_status_hide_button_text">Diese Transaktion ausblenden</string>
<string name="express_status_hide_dialog_text">Sobald der Transaktionsstatus ausgeblendet ist, kann er nicht mehr angezeigt werden. Stattdessen kannst Du ihn durch einfaches Wischen schließen.</string>
<string name="express_status_hide_dialog_title">Diese Transaktion ausblenden</string>
<string name="express_swap_with">Tauschen mit</string>
<string name="express_token_list_empty_search">Keine Token gefunden. Bitte versuche eine andere Anfrage</string>
<string name="express_transaction_id">ID: %s</string>
<string name="express_transaction_id_copied">Transaktions-ID kopiert</string>
@ -419,6 +422,7 @@
<string name="markets_add_to_my_portfolio_unavailable_description">Dieses Asset wird derzeit in der Wallet nicht unterstützt.</string>
<string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">Dieses Asset ist für dieses Wallet nicht verfügbar</string>
<string name="markets_add_token">Hinzufügen</string>
<string name="markets_apy_placeholder">APY %s</string>
<string name="markets_available_networks">Verfügbare Netzwerke</string>
<string name="markets_common_my_portfolio">Mein Portfolio</string>
<string name="markets_common_title">Markt</string>
@ -443,11 +447,13 @@
<string name="markets_selector_interval_7d_title">7T</string>
<string name="markets_selector_interval_all_title">Alle</string>
<string name="markets_sort_by_experienced_buyers_title">Erfahrene Käufer</string>
<string name="markets_sort_by_rating_title">Bewertung</string>
<string name="markets_sort_by_rating_title">Kapitalisierung</string>
<string name="markets_sort_by_title">Sortieren nach</string>
<string name="markets_sort_by_top_gainers_title">Top-Gewinner</string>
<string name="markets_sort_by_top_losers_title">Top-Verlierer</string>
<string name="markets_sort_by_trending_title">Beliebt</string>
<string name="markets_staking_banner_description_placeholder">Staking ist der einfachste Weg, um Belohnungen für Deine Kryptowährung zu erhalten. %s</string>
<string name="markets_staking_banner_title">Verdiene bis zu %s APY</string>
<string name="markets_token_details_about_token_title">Über %s</string>
<plurals name="markets_token_details_amount_exchanges">
<item quantity="one">%d börse</item>
@ -492,9 +498,9 @@
<string name="markets_token_details_market_capitalization">MarketCap</string>
<string name="markets_token_details_market_capitalization_description">Der Gesamtmarktwert einer Kryptowährung, berechnet sich durch Multiplikation des aktuellen Preises des Coins mit der Gesamtzahl der im Umlauf befindlichen Coins</string>
<string name="markets_token_details_market_capitalization_full">MarketCap</string>
<string name="markets_token_details_market_rating">Marktbewertung</string>
<string name="markets_token_details_market_rating">Marktposition</string>
<string name="markets_token_details_market_rating_description">Position im Krypto-Rating zwischen allen Coins basierend auf der Marktkapitalisierung</string>
<string name="markets_token_details_market_rating_full">Marktbewertung</string>
<string name="markets_token_details_market_rating_full">Marktposition</string>
<string name="markets_token_details_max_supply">Maximale Versorgung</string>
<string name="markets_token_details_max_supply_description">Die maximale Anzahl von Coins oder Tokens, die jemals für eine bestimmte Kryptowährung existieren können</string>
<string name="markets_token_details_max_supply_full">Maximale Versorgung</string>
@ -530,6 +536,13 @@
<string name="nft_details_base_information">Basisinformationen</string>
<string name="nft_details_chain">Netzwerk</string>
<string name="nft_details_contract_address">Vertragsadresse</string>
<string name="nft_details_info_chain">Chain ist die Blockchain, in der das NFT existiert.</string>
<string name="nft_details_info_contract_address">Die Vertragsadresse ist eine eindeutige Kennung für den Smart Contract, der die Token auf der Blockchain verwaltet.</string>
<string name="nft_details_info_rarity_label">Eine Bezeichnung, die beschreibt, wie selten das NFT ist. Je niedriger der Wert, desto einzigartiger das NFT.</string>
<string name="nft_details_info_rarity_rank">Die Position eines NFT im Seltenheitsranking unter anderen Token. Je höher der Rang, desto seltener und wertvoller ist das NFT.</string>
<string name="nft_details_info_token_address">Die Token-Adresse ist eine eindeutige Kennung für das Token in der Blockchain und ermöglicht die Verfolgung von Transaktionen und Eigentumsverhältnissen</string>
<string name="nft_details_info_token_id">Die Token-ID ist eine eindeutige Kennung, die jedem Token zugewiesen wird und es von anderen in der Sammlung unterscheidet.</string>
<string name="nft_details_info_token_standard">Der Token-Standard definiert, um welche Art von Token es sich handelt und wie es mit verschiedenen Wallets und Plattformen funktioniert</string>
<string name="nft_details_last_sale_price">Letzter Verkaufspreis</string>
<string name="nft_details_rarity_label">Raritätsetikett</string>
<string name="nft_details_rarity_rank">Seltenheitsgrad</string>
@ -542,11 +555,12 @@
<string name="nft_receive_available_section_title">Verfügbar</string>
<string name="nft_receive_choose_network">Netzwerk auswählen</string>
<string name="nft_receive_title">NFT erhalten</string>
<string name="nft_receive_unavailable_asset_warning_message">Du hast dieses Netzwerk noch nicht hinzugefügt. Um NFTs zu empfangen, füge es zum Hauptbildschirm hinzu!</string>
<string name="nft_receive_unavailable_asset_warning_message">Du hast dieses Netzwerk noch nicht hinzugefügt. Um NFTs zu erhalten, füge es Deinem Portfolio hinzu.</string>
<string name="nft_receive_unavailable_asset_warning_title">Netzwerk nicht hinzugefügt</string>
<string name="nft_receive_unavailable_section_title">Nicht hinzugefügt</string>
<string name="nft_send">NFT senden</string>
<string name="nft_traits_title">Eigenschaften</string>
<string name="nft_untitled_collection">Unbetitelte Sammlung</string>
<string name="nft_wallet_count">%1$d NFTs in der %2$d Sammlung</string>
<string name="nft_wallet_receive_nft">Tippe hier, um das erste NFT zu erhalten</string>
<string name="nft_wallet_title">NFT-Sammlungen</string>
@ -847,7 +861,7 @@
<string name="staking_amount_tron_integer_error_unstaking">Der Betrag der unstaked wird, wird aufgrund der Netzwerkregeln auf %1$s TRX gerundet.</string>
<string name="staking_claim_unstaked">Nicht gestakte beanspruche</string>
<string name="staking_details_account_fee">Gebühr für das Staking-Konto</string>
<string name="staking_details_account_fee_info">Ein Staking-Konto ist ein spezielles Konto, auf dem eingesetzte SOL-Token gelagert werden. Es wird erstellt, wenn du deine Token an einen Validator delegierst, um an der Transaktionsvalidierung teilzunehmen und Belohnungen zu verdienen. Für die Erstellung des Staking-Kontos wird eine geringe Gebühr erhoben, die nach Abschluss des Stakings zurückgegeben wird.</string>
<string name="staking_details_account_fee_info">Ein Staking-Konto ist ein spezielles Konto, auf dem eingesetzte SOL-Token gespeichert werden. Es wird erstellt, indem Sie Ihre Token an einen Validator delegieren, um an der Transaktionsvalidierung teilzunehmen und Belohnungen zu erhalten. Für die Einrichtung des Staking-Kontos wird eine geringe Gebühr erhoben, die nach Abschluss des Stakings zurückerstattet wird.</string>
<string name="staking_details_annual_percentage_rate">Jährliche prozentuale Rendite</string>
<string name="staking_details_annual_percentage_rate_info">Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst.</string>
<string name="staking_details_apr">Effektiver Jahreszins</string>
@ -856,7 +870,7 @@
<string name="staking_details_average_reward_rate">Durchschnittliche Belohnungsquote</string>
<string name="staking_details_banner_text">Was ist Staking?</string>
<string name="staking_details_estimated_profit">%s geschätzter Profit</string>
<string name="staking_details_market_rating">Marktbewertung</string>
<string name="staking_details_market_rating">Marktposition</string>
<string name="staking_details_metrics_block_header">Metriken</string>
<string name="staking_details_min_rewards_notification">Gemäß den Netzwerkregeln %1$s sind Ansprüche ab %2$s möglich. Die unten aufgeführten Beträge werden Deinem Konto nach der Aufhebung des Stakings gutgeschrieben.</string>
<string name="staking_details_minimum_requirement">Mindestanforderungen</string>
@ -882,15 +896,15 @@
<string name="staking_notification_additional_ada_deposit_text">Beim Staking im Cardano-Netzwerk wird Dein gesamtes Guthaben verwendet. Weitere 2 ADA werden reserviert und nach dem Unstaking zurückgegeben. Deine ADA bleiben während des Stakings freigeschaltet.</string>
<string name="staking_notification_additional_ada_deposit_title">Einzelheiten zum ADA-Staking</string>
<string name="staking_notification_claim_rewards_text">Verdiente Belohnungen werden an Deine Wallet gesendet und stehen Dir sofort zur Verfügung</string>
<string name="staking_notification_earn_rewards_text">Stake sicher und verdienen Belohnungen</string>
<string name="staking_notification_earn_rewards_text_daily">Sicher staken und tägliche Belohnungen verdienen.</string>
<string name="staking_notification_earn_rewards_text_hourly">Sicher staken und stündliche Belohnungen verdienen.</string>
<string name="staking_notification_earn_rewards_text_monthly">Sicher staken und monatliche Belohnungen verdienen.</string>
<string name="staking_notification_earn_rewards_text">Tangem ermöglicht es Benutzern, ihre Kryptowährungen zu Staken</string>
<string name="staking_notification_earn_rewards_text_daily">Tangem ermöglicht es Benutzern, ihre Kryptowährungen zu Staken</string>
<string name="staking_notification_earn_rewards_text_hourly">Tangem ermöglicht es Benutzern, ihre Kryptowährungen zu Staken</string>
<string name="staking_notification_earn_rewards_text_monthly">Tangem ermöglicht es Benutzern, ihre Kryptowährungen zu Staken</string>
<string name="staking_notification_earn_rewards_text_period_day">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden tag.</string>
<string name="staking_notification_earn_rewards_text_period_hour">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Stunde.</string>
<string name="staking_notification_earn_rewards_text_period_month">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden Monat.</string>
<string name="staking_notification_earn_rewards_text_period_week">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche.</string>
<string name="staking_notification_earn_rewards_text_weekly">Sicher staken und wöchentliche Belohnungen verdienen.</string>
<string name="staking_notification_earn_rewards_text_weekly">Tangem ermöglicht es Benutzern, ihre Kryptowährungen zu Staken</string>
<string name="staking_notification_earn_rewards_title">Verdiene Staking-Belohnungen</string>
<string name="staking_notification_low_staked_balance_text">Dein verbleibender Stakingbetrag ist zu niedrig, um den Einsatz aufzuheben. Du musst mehr Staken, um den Mindestbetrag zum Aufheben des Einsatzes zu erreichen.</string>
<string name="staking_notification_low_staked_balance_title">Niedriges Stakingguthaben</string>
@ -1026,7 +1040,7 @@
<string name="token_details_hide_alert_message">Du bist dabei, dieses Token vom Hauptbildschirm auszublenden. Du kannst es jederzeit über die Seite „Token verwalten“ wieder hinzufügen.</string>
<string name="token_details_hide_alert_title">Blende %s aus</string>
<string name="token_details_hide_token">Token ausblenden</string>
<string name="token_details_staking_block_subtitle">Durch das Staking kannst du alle %2$s Tage %1$s verdienen und Belohnungen erhalten</string>
<string name="token_details_staking_block_subtitle">Durch Staking kannst Du %1$s erhalten und alle %2$s Tage Belohnungen bekommen.</string>
<string name="token_details_staking_block_title">Staking-Dienst</string>
<string name="token_details_token_type_subtitle">%1$s Token in %%image%% %2$s Netzwerk</string>
<string name="token_details_token_type_subtitle_no_standard">Token in %%image%% %1$s Netzwerk</string>
@ -1051,6 +1065,8 @@
<string name="transaction_history_transaction_validator">Validierer: %s</string>
<string name="transfer_min_amount_error">Minimum %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">Der Mindesttransaktionsbetrag beträgt %1$s.</string>
<string name="tron_will_be_send_token_fee_description">Die Netzwerkgebühren für beliebte Token im Tron-Netzwerk können höher sein. Das Staking von TRX kann helfen, die Transaktionskosten zu senken.</string>
<string name="tron_will_be_send_token_fee_title">Spare bei den Tron-Netzwerkgebühren</string>
<string name="try_to_load_data_again_button_title">Versuche es erneut</string>
<string name="twin_error_same_card">Du hast dieselbe Karte oder Ring gescannt. Um ein Zwillings-Wallet zu erstellen, musst du die Karte oder Ring mit der Nummer %d scannen.</string>
<string name="twin_error_wrong_twin">Du hast die falsche Doppelkarte oder Ring gescannt. Bitte versuche eine andere Karte oder Ring</string>
@ -1145,6 +1161,8 @@
<string name="wallet_promo_banner_button_title">Jetzt mit 10 % Rabatt kaufen</string>
<string name="wallet_promo_banner_description">Greife auf 13.000+ Kryptowährungen zu. Kaufe, verkaufe, tausche und stake Sie mit einem einzigen Fingertipp.\nVerbinde bis zu drei Karten für ein Backup.</string>
<string name="wallet_promo_banner_title">Entdecke die Tangem Wallet</string>
<string name="wallet_settings_push_notifications_description">Bleib stets über eingehende Wallet-Transaktionen und Tangem-Updates auf dem Laufenden.</string>
<string name="wallet_settings_push_notifications_title">Push-Benachrichtigungen</string>
<string name="wallet_settings_title">Wallet-Einstellungen</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Verwende %s oder scanne eine Karte oder Ring, um den Zugriff auf deine Wallet freizuschalten.</string>
@ -1172,7 +1190,7 @@
<string name="warning_express_approval_in_progress_message">Die Genehmigung des Swaps ist im Gange und wird in Kürze abgeschlossen sein.</string>
<string name="warning_express_approval_in_progress_title">Genehmigung in Arbeit</string>
<string name="warning_express_dust_message">Der Mindestbetrag für den Tausch beträgt %1$s. Bitte stelle sicher, dass der Restsaldo nach dem Swap nicht unter %2$s liegt.</string>
<string name="warning_express_no_exchangeable_coins_description">Du hast keine %s Coins in deiner Liste</string>
<string name="warning_express_no_exchangeable_coins_description">Du hast keine %s austauschbaren Coins in Deiner Liste</string>
<string name="warning_express_no_exchangeable_coins_title">Keine Token zum Tauschen verfügbar</string>
<string name="warning_express_not_enough_fee_for_token_tx_description">Um eine Transaktion durchzuführen, du etwas etwas einzahlen %1$s %2$s</string>
<string name="warning_express_not_enough_fee_for_token_tx_title">Die Gebühr %s kann nicht gedeckt werden</string>
@ -1237,6 +1255,7 @@
<string name="wc_alert_connection_timeout_description">Zeitüberschreitungsfehler. Bitte versuche es später erneut.</string>
<string name="wc_alert_connection_timeout_title">WalletConnect konnte nicht hergestellt werden</string>
<string name="wc_alert_domain_issues_description">Diese Domäne kann nicht verifiziert werden. Überprüfe die Anfrage sorgfältig und bestätige diese dann.</string>
<string name="wc_alert_session_disconnected_description">Bitte kehre zu Deinem Browser zurück und stellen die Verbindung über WalletConnect erneut her.</string>
<string name="wc_alert_session_disconnected_title">WalletConnect-Sitzung wurde getrennt</string>
<string name="wc_alert_unknown_error_description">Fehlercode: %s. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support.</string>
<string name="wc_alert_unknown_error_title">Wir haben einen unbekannten Fehler festgestellt.</string>
@ -1244,17 +1263,30 @@
<string name="wc_alert_unsupported_networks_title">Nicht unterstützte Netzwerke</string>
<string name="wc_alert_verified_domain_description">Tangem unterstützt ein erforderliches Netzwerk um %s.</string>
<string name="wc_alert_verified_domain_title">Verifizierte Domain</string>
<string name="wc_alert_wrong_card_description">Falsche Karte oder falscher Ring in der App ausgewählt</string>
<string name="wc_alert_wrong_card_title">Wir haben eine Art Problem</string>
<string name="wc_allow_to_spend">Erlaubnis auszugeben</string>
<string name="wc_common_address">Vertragsadresse</string>
<string name="wc_common_connect">Verbinden</string>
<string name="wc_common_network">Netzwerk</string>
<string name="wc_common_networks">Netzwerke</string>
<string name="wc_common_wallet">Wallet</string>
<string name="wc_connected_networks">Verbundene Netzwerke</string>
<string name="wc_connection_reqeust_can_view_balance">Anzeige des Kontostands und der Aktivitäten in Deiner Wallet</string>
<string name="wc_connection_reqeust_cant_sign">Signiere die Transaktionen ohne eine Vorankündigung</string>
<string name="wc_connection_reqeust_request_approval">Genehmigung für Transaktionen anfordern</string>
<string name="wc_connection_reqeust_will_not">Wird nicht in der Lage sein,</string>
<string name="wc_connection_reqeust_would_like">Möchte</string>
<string name="wc_connection_request">Verbindungsanfrage</string>
<string name="wc_connections">Verbindungen</string>
<string name="wc_contents">Inhalt</string>
<string name="wc_copy_data_button_text">Daten kopieren</string>
<string name="wc_custom_allowance_title">Benutzerdefinierter Freibetrag</string>
<string name="wc_disconnect_all">Alle trennen</string>
<string name="wc_disconnect_all_alert_desc">Text über die Trennung aller dApps</string>
<string name="wc_disconnect_all_alert_title">Alle dApps trennen</string>
<string name="wc_estimated_wallet_changes">Geschätzte Wallet-Änderungen</string>
<string name="wc_malicious_transaction">Böswillige/ gefährliche Transaktion</string>
<string name="wc_missing_required_network_description">Füge Deinem Profil für dieses Wallet das Netzwerk %s hinzu</string>
<string name="wc_missing_required_network_title">Die Wallet verfügt über keine erforderlichen Netzwerke</string>
<string name="wc_new_connection">Neue Verbindung</string>
@ -1262,6 +1294,13 @@
<string name="wc_no_sessions_title">Keine Sitzungen</string>
<string name="wc_notification_security_risk_subtitle">Diese Domain wird von mehreren Sicherheitsanbietern als unsicher eingestuft. Verlasse diese umgehend, um Dein Vermögen zu schützen.</string>
<string name="wc_notification_security_risk_title">Bekanntes Sicherheitsrisiko</string>
<string name="wc_request_from">Anfrage von</string>
<string name="wc_signature_type">Art der Signatur</string>
<string name="wc_transaction_info_to_title">An</string>
<string name="wc_transaction_request">Transaktionsanfrage</string>
<string name="wc_transaction_request_title">Transaktionsanfrage</string>
<string name="wc_unlimited_amount">Unbegrenzte Menge</string>
<string name="wc_wallet_connect">Wallet verbinden</string>
<string name="welcome_interrupted_backup_alert_discard">Verwerfen</string>
<string name="welcome_interrupted_backup_alert_message">Du hast eine unterbrochene Sicherung. Möchtest du diese fortsetzen?</string>
<string name="welcome_interrupted_backup_alert_resume">Ja, fortsetzen</string>

View file

@ -192,6 +192,8 @@
<string name="common_server_unavailable">サーバーが利用できません。しばらくしてからもう一度お試しください。</string>
<string name="common_share">共有</string>
<string name="common_share_link">リンクを共有</string>
<string name="common_show_less">詳細を非表示</string>
<string name="common_show_more">もっと見る</string>
<string name="common_sign">署名</string>
<string name="common_sign_and_send">署名して送信</string>
<string name="common_stake">ステーキング</string>
@ -292,25 +294,25 @@
<string name="express_exchange_notification_verification_text">確認するには、プロバイダーのウェブサイトにアクセスしてください。</string>
<string name="express_exchange_notification_verification_title">プロバイダーによる本人確認手続きが必要です。</string>
<string name="express_exchange_status_bought">買付完了</string>
<string name="express_exchange_status_buying">買付</string>
<string name="express_exchange_status_buying">買付待ち</string>
<string name="express_exchange_status_buying_active">買付中...</string>
<string name="express_exchange_status_canceled">キャンセルされました</string>
<string name="express_exchange_status_confirmed">確認済み</string>
<string name="express_exchange_status_confirming">確認</string>
<string name="express_exchange_status_confirming_active">確認...</string>
<string name="express_exchange_status_exchanged">交換済み</string>
<string name="express_exchange_status_exchanging">交換</string>
<string name="express_exchange_status_exchanging_active">交換中...</string>
<string name="express_exchange_status_failed">失敗しました</string>
<string name="express_exchange_status_paused">停止中</string>
<string name="express_exchange_status_canceled">取引はキャンセルされました</string>
<string name="express_exchange_status_confirmed">入金確認済み</string>
<string name="express_exchange_status_confirming">確認待ち</string>
<string name="express_exchange_status_confirming_active">確認待ち...</string>
<string name="express_exchange_status_exchanged">交換完了</string>
<string name="express_exchange_status_exchanging">交換待ち</string>
<string name="express_exchange_status_exchanging_active">スワップ待ち...</string>
<string name="express_exchange_status_failed">交換に失敗しました</string>
<string name="express_exchange_status_paused">交換は一時停止されました</string>
<string name="express_exchange_status_received">入金済み</string>
<string name="express_exchange_status_receiving">入金待ち</string>
<string name="express_exchange_status_receiving_active">入金待ち...</string>
<string name="express_exchange_status_refunded">返金済み</string>
<string name="express_exchange_status_refunding">処理中</string>
<string name="express_exchange_status_refunded">払い戻し完了</string>
<string name="express_exchange_status_refunding">払い戻し待ち</string>
<string name="express_exchange_status_sending">あなたへ送金しています</string>
<string name="express_exchange_status_sending_active">送金中...</string>
<string name="express_exchange_status_sent">送金済み</string>
<string name="express_exchange_status_sending_active">資金の送金中...</string>
<string name="express_exchange_status_sent">送金済み資金</string>
<string name="express_exchange_status_subtitle">プロバイダー提供のデータ。推定額は市場動向により変更される場合があります。</string>
<string name="express_exchange_status_title">交換ステータス</string>
<string name="express_exchange_status_verifying">確認が必要です</string>
@ -334,6 +336,7 @@
<string name="express_status_hide_button_text">この取引を非表示にする</string>
<string name="express_status_hide_dialog_text">一度非表示にすると、取引状況を再度表示することはできません。代わりに、スワイプして閉じることができます。</string>
<string name="express_status_hide_dialog_title">取引状況を非表示にしますか?</string>
<string name="express_swap_with">スワップする</string>
<string name="express_token_list_empty_search">トークンが見つかりません。別のリクエストをお試しください。</string>
<string name="express_transaction_id">ID: %s</string>
<string name="express_transaction_id_copied">取引IDをコピーしました</string>
@ -446,7 +449,6 @@
<string name="markets_sort_by_top_losers_title">下落率上位</string>
<string name="markets_sort_by_trending_title">トレンド</string>
<string name="markets_staking_banner_description_placeholder">ステーキングは暗号資産で報酬を受け取る最も簡単な方法です。 %s</string>
<string name="markets_staking_banner_description_show_more">もっと見る</string>
<string name="markets_staking_banner_title">最大%s APYを獲得</string>
<string name="markets_token_details_about_token_title">%sについて</string>
<plurals name="markets_token_details_amount_exchanges">
@ -527,6 +529,13 @@
<string name="nft_details_base_information">基本情報</string>
<string name="nft_details_chain">チェーン</string>
<string name="nft_details_contract_address">コントラクトアドレス</string>
<string name="nft_details_info_chain">チェーンは、NFTが存在するブロックチェーンです。</string>
<string name="nft_details_info_contract_address">コントラクトアドレスは、ブロックチェーン上のトークンを管理するスマートコントラクトの識別子です。</string>
<string name="nft_details_info_rarity_label">NFTの希少性を示すラベル。数値が低いほどユニークなNFTであることを示します。</string>
<string name="nft_details_info_rarity_rank">他のトークンの中での希少性ランキングにおけるNFTの位置。ランクが高ければ高いほど、NFTの希少性と価値は高くなります。</string>
<string name="nft_details_info_token_address">トークンアドレスは、ブロックチェーン上のトークンの識別子であり、取引と所有権の追跡を可能にします</string>
<string name="nft_details_info_token_id">トークンIDは、各トークンに割り当てられる識別子であり、コレクション内の他のトークンと区別されます</string>
<string name="nft_details_info_token_standard">トークン標準は、トークンの種類と、さまざまなウォレットやプラットフォームでどのように機能するかを定義します</string>
<string name="nft_details_last_sale_price">最終販売価格</string>
<string name="nft_details_rarity_label">レアリティ・ラベル</string>
<string name="nft_details_rarity_rank">レアリティ・ランク</string>
@ -539,11 +548,12 @@
<string name="nft_receive_available_section_title">利用可能</string>
<string name="nft_receive_choose_network">ネットワークを選択</string>
<string name="nft_receive_title">NFTを受け取る</string>
<string name="nft_receive_unavailable_asset_warning_message">このネットワークはまだ追加されていません。NFTの受け取りを開始するには、メイン画面に追加してください。</string>
<string name="nft_receive_unavailable_asset_warning_message">このネットワークはまだ追加されていません。NFTを受け取るには、ポートフォリオに追加してください。</string>
<string name="nft_receive_unavailable_asset_warning_title">ネットワークが追加されていません</string>
<string name="nft_receive_unavailable_section_title">未追加</string>
<string name="nft_send">NFTを送信する</string>
<string name="nft_traits_title">特徴</string>
<string name="nft_untitled_collection">無題のコレクション</string>
<string name="nft_wallet_count">%1$dコレクションの%2$dNFT</string>
<string name="nft_wallet_receive_nft">ここをタップして最初のNFTを受け取ります</string>
<string name="nft_wallet_title">NFTコレクション</string>
@ -1044,7 +1054,7 @@
<string name="transaction_history_transaction_validator">バリデーター: %s</string>
<string name="transfer_min_amount_error">最小%s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">最小取引金額は%1$sです。</string>
<string name="tron_will_be_send_token_fee_description">Tronネットワークの人気トークンの手数料は高めです。TRXをステーキングすると、より安く、あるいは無料で取引できます。</string>
<string name="tron_will_be_send_token_fee_description">Tronネットワークの人気トークンの手数料は高い可能性があります。TRXをステーキングすると、取引コストを削減できる可能性があります。</string>
<string name="tron_will_be_send_token_fee_title">Tronネットワーク手数料を節約</string>
<string name="try_to_load_data_again_button_title">もう一度やり直してください</string>
<string name="twin_error_same_card">同じカードをスキャンしました。ツインウォレットを作成するには、番号%dのカードをスキャンする必要があります。</string>
@ -1136,6 +1146,8 @@
<string name="wallet_promo_banner_button_title">今すぐ10 % オフで購入</string>
<string name="wallet_promo_banner_description">1.3万種類以上の暗号資産にアクセス。ワンタップで買付、売却、スワップ、ステーキングが可能です。\nバックアップ用に最大3枚のカードを連携できます。</string>
<string name="wallet_promo_banner_title">Tangemウォレットを見る</string>
<string name="wallet_settings_push_notifications_description">ウォレットの受信取引とTangemの更新について通知を受け取る。</string>
<string name="wallet_settings_push_notifications_title">プッシュ通知</string>
<string name="wallet_settings_title">ウォレット設定</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">%sを使用するか、カード / リングをスキャンしてウォレットにアクセスしてください</string>
@ -1227,7 +1239,7 @@
<string name="wc_alert_connection_timeout_description">タイムアウトエラーが発生しました。しばらくしてからもう一度お試しください。</string>
<string name="wc_alert_connection_timeout_title">WalletConnectを確立できませんでした</string>
<string name="wc_alert_domain_issues_description">このドメインは検証できません。承認前にリクエスト内容をよく確認してください。</string>
<string name="wc_alert_session_disconnected_description">ブラウザに戻り、WalletConnect経由で再接続してください。</string>
<string name="wc_alert_session_disconnected_description">ブラウザに戻り、WalletConnect経由で再接続してください。</string>
<string name="wc_alert_session_disconnected_title">WalletConnectセッションが接続解除されました</string>
<string name="wc_alert_unknown_error_description">エラーコード: %s 。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。</string>
<string name="wc_alert_unknown_error_title">不明なエラーが発生しました</string>
@ -1235,8 +1247,10 @@
<string name="wc_alert_unsupported_networks_title">サポートされていないネットワーク</string>
<string name="wc_alert_verified_domain_description">Tangemは%sで必要なネットワークをサポートします。</string>
<string name="wc_alert_verified_domain_title">検証済みドメイン</string>
<string name="wc_alert_wrong_card_description">Tangemアプリで誤ったカードまたはリングが選択されました</string>
<string name="wc_alert_wrong_card_description">アプリで間違ったカードまたはリングが選択されました</string>
<string name="wc_alert_wrong_card_title">問題が起きています</string>
<string name="wc_allow_to_spend">使用を許可する</string>
<string name="wc_common_address">アドレス</string>
<string name="wc_common_connect">接続する</string>
<string name="wc_common_network">ネットワーク</string>
<string name="wc_common_networks">ネットワーク</string>
@ -1249,9 +1263,14 @@
<string name="wc_connection_reqeust_would_like">したい</string>
<string name="wc_connection_request">接続リクエスト</string>
<string name="wc_connections">接続</string>
<string name="wc_contents">内容</string>
<string name="wc_copy_data_button_text">データをコピー</string>
<string name="wc_custom_allowance_title">使用可能量の設定</string>
<string name="wc_disconnect_all">すべての接続を解除する</string>
<string name="wc_disconnect_all_alert_desc">すべてのdAppsの接続解除に関するテキスト</string>
<string name="wc_disconnect_all_alert_title">すべてのdAppを接続解除する</string>
<string name="wc_estimated_wallet_changes">ウォレットの変更の予測</string>
<string name="wc_malicious_transaction">悪意のある取引</string>
<string name="wc_missing_required_network_description">このウォレットのプロフィールに%sネットワークを追加する</string>
<string name="wc_missing_required_network_title">ウォレットに必要なネットワークはありません</string>
<string name="wc_new_connection">新しい接続</string>
@ -1259,6 +1278,12 @@
<string name="wc_no_sessions_title">セッションなし</string>
<string name="wc_notification_security_risk_subtitle">このドメインは複数のセキュリティプロバイダーから安全でないとの警告を受けています。あなたの資産を守るため、直ちにアクセスを中止してください。</string>
<string name="wc_notification_security_risk_title">既知のセキュリティリスク</string>
<string name="wc_request_from">リクエスト元</string>
<string name="wc_signature_type">署名タイプ</string>
<string name="wc_transaction_info_to_title">宛先</string>
<string name="wc_transaction_request">取引リクエスト</string>
<string name="wc_transaction_request_title">取引リクエスト</string>
<string name="wc_unlimited_amount">無制限</string>
<string name="wc_wallet_connect">ウォレットコネクト</string>
<string name="welcome_interrupted_backup_alert_discard">破棄</string>
<string name="welcome_interrupted_backup_alert_message">バックアップが中断されました。再開しますか?</string>

View file

@ -167,6 +167,7 @@
<string name="common_network_fee_title">Сетевая комиссия</string>
<string name="common_network_fee_warning_content">Сумма отправки будет уменьшена на %1$s (%2$s) для покрытия выбранного уровня комиссии</string>
<string name="common_next">Далее</string>
<string name="common_nft">NFT</string>
<string name="common_no">Нет</string>
<string name="common_no_address">Нет адреса</string>
<string name="common_now">Сейчас</string>
@ -189,6 +190,7 @@
<string name="common_search">Поиск</string>
<string name="common_search_tokens">Поиск токенов</string>
<string name="common_second_no_param">сек</string>
<string name="common_see_all">Показать всё</string>
<string name="common_seed_phrase">Seed-фраза</string>
<string name="common_select_action">Выберите действие</string>
<string name="common_sell">Продать</string>
@ -197,6 +199,7 @@
<string name="common_server_unavailable">Сервер недоступен, повторите попытку позднее</string>
<string name="common_share">Поделиться</string>
<string name="common_share_link">Поделиться ссылкой</string>
<string name="common_show_more">Показать больше</string>
<string name="common_sign">Подписать</string>
<string name="common_sign_and_send">Подписать и отправить</string>
<string name="common_stake">Застейкать</string>
@ -295,25 +298,26 @@
<string name="express_exchange_notification_refund_title">Сумма была возвращена в %1$s (%2$s сети)</string>
<string name="express_exchange_notification_verification_text">Посетите сайт провайдера для проверки</string>
<string name="express_exchange_notification_verification_title">Провайдер запрашивает прохождение верификации</string>
<string name="express_exchange_status_bought">Куплено</string>
<string name="express_exchange_status_buying">Покупка</string>
<string name="express_exchange_status_buying_active">Покупка...</string>
<string name="express_exchange_status_canceled">Отменен</string>
<string name="express_exchange_status_confirmed">Подтверждено</string>
<string name="express_exchange_status_confirming">Подтверждение</string>
<string name="express_exchange_status_confirming_active">Подтверждение...</string>
<string name="express_exchange_status_exchanged">Обменяно</string>
<string name="express_exchange_status_exchanging">Обмен</string>
<string name="express_exchange_status_exchanging_active">Обмен...</string>
<string name="express_exchange_status_failed">Неудачно</string>
<string name="express_exchange_status_paused">Приостановлен</string>
<string name="express_exchange_status_bought">Покупка завершена</string>
<string name="express_exchange_status_buying">Ожидание покупки</string>
<string name="express_exchange_status_buying_active">Ожидание покупки...</string>
<string name="express_exchange_status_canceled">Транзакция отменена</string>
<string name="express_exchange_status_confirmed">Депозит подтверждён</string>
<string name="express_exchange_status_confirming">Ожидает подтверждения</string>
<string name="express_exchange_status_confirming_active">Ожидает подтверждения...</string>
<string name="express_exchange_status_exchanged">Обмен завершён</string>
<string name="express_exchange_status_exchanging">Ожидает обмена</string>
<string name="express_exchange_status_exchanging_active">Ожидает обмена...</string>
<string name="express_exchange_status_failed">Ошибка обмена</string>
<string name="express_exchange_status_paused">Обмен остановлен</string>
<string name="express_exchange_status_received">Депозит получен</string>
<string name="express_exchange_status_receiving">Ожидание депозита</string>
<string name="express_exchange_status_receiving_active">Ожидаем пополнения...</string>
<string name="express_exchange_status_refunded">Возвращено</string>
<string name="express_exchange_status_sending">Отправляем </string>
<string name="express_exchange_status_refunded">Средства возвращены</string>
<string name="express_exchange_status_refunding">Возврат средств</string>
<string name="express_exchange_status_sending">Отправка средств</string>
<string name="express_exchange_status_sending_active">Отправка средств...</string>
<string name="express_exchange_status_sent">Отправлено</string>
<string name="express_exchange_status_sent">Средства отправлены</string>
<string name="express_exchange_status_subtitle">Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий.</string>
<string name="express_exchange_status_title">Статус обмена</string>
<string name="express_exchange_status_verifying">Требуется верификация</string>
@ -450,6 +454,8 @@
<string name="markets_sort_by_top_gainers_title">Лидеры роста</string>
<string name="markets_sort_by_top_losers_title">Лидеры падения</string>
<string name="markets_sort_by_trending_title">В тренде</string>
<string name="markets_staking_banner_description_placeholder">Стейкинг — простой способ получать доход с вашей криптовалюты. %s</string>
<string name="markets_staking_banner_title">Получайте до %s APY</string>
<string name="markets_token_details_about_token_title">О %s</string>
<plurals name="markets_token_details_amount_exchanges">
<item quantity="one">%d биржа</item>
@ -521,6 +527,51 @@
<string name="markets_tooltip_message">Потяните вверх или коснитесь поисковой строки, чтобы добавить токены напрямую из рынка</string>
<string name="markets_tooltip_title">Добавить токены</string>
<string name="nfc_error_unavailable">Функция NFC недоступна на вашем устройстве</string>
<string name="nft_about_title">Об NFT</string>
<string name="nft_asset">NFT</string>
<plurals name="nft_collections_count">
<item quantity="one">NFT</item>
<item quantity="few">NFT</item>
<item quantity="many">NFT</item>
<item quantity="other">NFT</item>
</plurals>
<string name="nft_collections_empty_description">Здесь будут отображаться NFT, полученные на ваш кошелёк</string>
<string name="nft_collections_empty_title">У вас ещё нет коллекций</string>
<string name="nft_collections_receive">Получить NFT</string>
<string name="nft_collections_title">NFT коллекции</string>
<string name="nft_collections_warning_subtitle">Часть данных может не отобразиться</string>
<string name="nft_collections_warning_title">Не удалось загрузить данные</string>
<string name="nft_details_base_information">Базовая информация</string>
<string name="nft_details_chain">Сеть</string>
<string name="nft_details_contract_address">Адрес контракта</string>
<string name="nft_details_info_chain">Сеть — это блокчейн, на котором существует NFT.</string>
<string name="nft_details_info_contract_address">Адрес контракта — уникальный код, который идентифицирует смарт-контракт, управляющий токенами в блокчейне</string>
<string name="nft_details_info_rarity_label">Метка, которая описывает, насколько редким является NFT. Чем ниже значение, тем уникальнее NFT.</string>
<string name="nft_details_info_rarity_rank">Позиция NFT в рейтинге редкости среди других токенов. Чем выше ранг, тем реже и ценнее NFT.</string>
<string name="nft_details_info_token_address">Адрес токена — это уникальный идентификатор токена в блокчейне, который позволяет отслеживать транзакции и владение им.</string>
<string name="nft_details_info_token_id">ID токена — уникальный идентификатор, присваиваемый каждому токену, который отличает его от других в коллекции</string>
<string name="nft_details_info_token_standard">Стандарт токена определяет его тип и как он работает с различными кошельками и платформами</string>
<string name="nft_details_last_sale_price">Последняя цена продажи</string>
<string name="nft_details_rarity_label">Метка уникальности</string>
<string name="nft_details_rarity_rank">Ранг редкости</string>
<string name="nft_details_token_address">Адрес токена</string>
<string name="nft_details_token_id">ID токена</string>
<string name="nft_details_token_standard">Стандарт токена</string>
<string name="nft_details_traits">Черты</string>
<string name="nft_empty_search">Нет результатов. Пожалуйста, попробуйте другой запрос.</string>
<string name="nft_no_collection">Нет коллекции</string>
<string name="nft_receive_available_section_title">Доступно</string>
<string name="nft_receive_choose_network">Выберите сеть</string>
<string name="nft_receive_title">Получить NFT</string>
<string name="nft_receive_unavailable_asset_warning_message">Вы ещё не добавили эту сеть. Чтобы получать NFT, добавьте её в ваш портфель.</string>
<string name="nft_receive_unavailable_asset_warning_title">Сеть не добавлена</string>
<string name="nft_receive_unavailable_section_title">Не добавлено</string>
<string name="nft_send">Отправить NFT</string>
<string name="nft_traits_title">Черты</string>
<string name="nft_wallet_count">%1$d NFTs в %2$d коллекциях</string>
<string name="nft_wallet_receive_nft">Нажмите здесь, чтобы получить первую NFT</string>
<string name="nft_wallet_title">NFT коллекции</string>
<string name="nft_wallet_unable_to_load">Невозможно загрузить данные</string>
<string name="no_account_generic">Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе</string>
<string name="no_account_polkadot">Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта.</string>
<string name="no_account_send_to_create">Для создания аккаунта отправьте средства на этот адрес</string>
@ -1024,6 +1075,8 @@
<string name="transaction_history_transaction_validator">валидатор: %s</string>
<string name="transfer_min_amount_error">Минимум %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">Минимальная сумма транзакции равна %1$s.</string>
<string name="tron_will_be_send_token_fee_description">Комиссии сети Tron для популярных токенов могут быть выше. Стейкинг TRX может помочь снизить расходы на транзакции.</string>
<string name="tron_will_be_send_token_fee_title">Экономьте на Tron комиссиях</string>
<string name="try_to_load_data_again_button_title">Попробовать снова</string>
<string name="twin_error_same_card">Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d</string>
<string name="twin_error_wrong_twin">Вы отсканировали не ту twin-карту. Пожалуйста, попробуйте отсканировать другую</string>
@ -1184,6 +1237,20 @@
<string name="warning_testnet_card_message">Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки.</string>
<string name="warning_testnet_card_title">Только для целей тестирования</string>
<string name="warning_token_balance_not_updated">Возможно, баланс устарел. Обновите страницу.</string>
<string name="wc_alert_session_disconnected_description">Пожалуйста, вернитесь в браузер и выполните повторное подключение через WalletConnect.</string>
<string name="wc_alert_wrong_card_description">Выбрана не верная карта или кольцо</string>
<string name="wc_common_address">Адрес</string>
<string name="wc_common_connect">Подключение</string>
<string name="wc_connection_reqeust_can_view_balance">Посмотреть баланс кошелька и его активность</string>
<string name="wc_connection_request">Запрос на подключение</string>
<string name="wc_contents">Вложение</string>
<string name="wc_copy_data_button_text">Копировать данные</string>
<string name="wc_request_from">Запрос от</string>
<string name="wc_signature_type">Тип подписи</string>
<string name="wc_transaction_info_to_title">На</string>
<string name="wc_transaction_request">Запрос транзакции</string>
<string name="wc_transaction_request_title">Запрос транзакции</string>
<string name="wc_wallet_connect">Подключение кошелька</string>
<string name="welcome_interrupted_backup_alert_discard">Отказаться</string>
<string name="welcome_interrupted_backup_alert_message">Вы не закончили резервное копирование. Хотите продолжить?</string>
<string name="welcome_interrupted_backup_alert_resume">Да, возобновить</string>

View file

@ -195,6 +195,8 @@
<string name="common_server_unavailable">The server is not available, please try again later</string>
<string name="common_share">Share</string>
<string name="common_share_link">Share Link</string>
<string name="common_show_less">Show less</string>
<string name="common_show_more">Show more</string>
<string name="common_sign">Sign</string>
<string name="common_sign_and_send">Sign and send</string>
<string name="common_stake">Stake</string>
@ -295,26 +297,26 @@
<string name="express_exchange_notification_refund_title">The amount was refunded in %1$s (%2$s network)</string>
<string name="express_exchange_notification_verification_text">Visit providers website for verification</string>
<string name="express_exchange_notification_verification_title">KYC verification required by provider</string>
<string name="express_exchange_status_bought">Bought</string>
<string name="express_exchange_status_buying">Buying</string>
<string name="express_exchange_status_buying_active">Buying...</string>
<string name="express_exchange_status_canceled">Canceled</string>
<string name="express_exchange_status_confirmed">Confirmed</string>
<string name="express_exchange_status_confirming">Confirming</string>
<string name="express_exchange_status_confirming_active">Confirming...</string>
<string name="express_exchange_status_exchanged">Exchanged</string>
<string name="express_exchange_status_exchanging">Exchanging</string>
<string name="express_exchange_status_exchanging_active">Exchanging...</string>
<string name="express_exchange_status_failed">Failed</string>
<string name="express_exchange_status_paused">Paused</string>
<string name="express_exchange_status_bought">Purchase completed</string>
<string name="express_exchange_status_buying">Awaiting Purchase</string>
<string name="express_exchange_status_buying_active">Awaiting Purchase...</string>
<string name="express_exchange_status_canceled">Transaction canceled</string>
<string name="express_exchange_status_confirmed">Deposit confirmed</string>
<string name="express_exchange_status_confirming">Waiting for confirmation</string>
<string name="express_exchange_status_confirming_active">Waiting for confirmation...</string>
<string name="express_exchange_status_exchanged">Exchange completed</string>
<string name="express_exchange_status_exchanging">Waiting for exchange</string>
<string name="express_exchange_status_exchanging_active">Waiting for exchange...</string>
<string name="express_exchange_status_failed">Exchange failed</string>
<string name="express_exchange_status_paused">Exchange paused</string>
<string name="express_exchange_status_received">Deposit received</string>
<string name="express_exchange_status_receiving">Awaiting deposit</string>
<string name="express_exchange_status_receiving">Waiting for deposit</string>
<string name="express_exchange_status_receiving_active">Awaiting deposit...</string>
<string name="express_exchange_status_refunded">Refunded</string>
<string name="express_exchange_status_refunding">Refunding</string>
<string name="express_exchange_status_refunded">Refund completed</string>
<string name="express_exchange_status_refunding">Waiting for refund</string>
<string name="express_exchange_status_sending">Sending to you</string>
<string name="express_exchange_status_sending_active">Sending to you...</string>
<string name="express_exchange_status_sent">Sent</string>
<string name="express_exchange_status_sending_active">Sending funds...</string>
<string name="express_exchange_status_sent">Funds sent</string>
<string name="express_exchange_status_subtitle">Provider-sourced data. Estimated amount subject to change due to market conditions.</string>
<string name="express_exchange_status_title">Exchange status</string>
<string name="express_exchange_status_verifying">Verification required</string>
@ -338,6 +340,7 @@
<string name="express_status_hide_button_text">Hide this transaction</string>
<string name="express_status_hide_dialog_text">Once hidden, the transaction status cannot be viewed again. You can simply swipe to dismiss instead.</string>
<string name="express_status_hide_dialog_title">Hide Transaction Status?</string>
<string name="express_swap_with">Swap with</string>
<string name="express_token_list_empty_search">No tokens found. Please try another request</string>
<string name="express_transaction_id">ID: %s</string>
<string name="express_transaction_id_copied">Transaction ID copied</string>
@ -451,7 +454,6 @@
<string name="markets_sort_by_top_losers_title">Top Losers</string>
<string name="markets_sort_by_trending_title">Trending</string>
<string name="markets_staking_banner_description_placeholder">Staking is the easiest way to receive rewards on your crypto. %s</string>
<string name="markets_staking_banner_description_show_more">Show more</string>
<string name="markets_staking_banner_title">Earn up to %s APY</string>
<string name="markets_token_details_about_token_title">About %s</string>
<plurals name="markets_token_details_amount_exchanges">
@ -535,13 +537,13 @@
<string name="nft_details_base_information">Base information</string>
<string name="nft_details_chain">Chain</string>
<string name="nft_details_contract_address">Contract Address</string>
<string name="nft_details_info_chain">stub</string>
<string name="nft_details_info_contract_address">stub</string>
<string name="nft_details_info_rarity_label">stub</string>
<string name="nft_details_info_rarity_rank">stub</string>
<string name="nft_details_info_token_address">stub</string>
<string name="nft_details_info_token_id">stub</string>
<string name="nft_details_info_token_standard">stub</string>
<string name="nft_details_info_chain">Chain is the blockchain where the NFT exists.</string>
<string name="nft_details_info_contract_address">The contract address is a unique identifier for the smart contract that governs the tokens on the blockchain</string>
<string name="nft_details_info_rarity_label">A label that describes how rare the NFT is. The lower the value, the more unique the NFT.</string>
<string name="nft_details_info_rarity_rank">The position of an NFT in the rarity ranking among other tokens. The higher the rank, the rarer and more valuable the NFT.</string>
<string name="nft_details_info_token_address">The token address is a unique identifier for the token on the blockchain, allowing tracking of transactions and ownership</string>
<string name="nft_details_info_token_id">The token ID is a unique identifier assigned to each token, distinguishing it from others in the collection</string>
<string name="nft_details_info_token_standard">Token standard defines what type of token it is and how it works with different wallets and platforms</string>
<string name="nft_details_last_sale_price">Last sale price</string>
<string name="nft_details_rarity_label">Rarity label</string>
<string name="nft_details_rarity_rank">Rarity rank</string>
@ -554,11 +556,12 @@
<string name="nft_receive_available_section_title">Available</string>
<string name="nft_receive_choose_network">Choose network</string>
<string name="nft_receive_title">Receive NFT</string>
<string name="nft_receive_unavailable_asset_warning_message">You haven\'t added this network yet. To start receiving NFTs, add it to the main screen!</string>
<string name="nft_receive_unavailable_asset_warning_message">You haven\'t added this network yet. To receive NFTs, add it to your portfolio.</string>
<string name="nft_receive_unavailable_asset_warning_title">Network not added</string>
<string name="nft_receive_unavailable_section_title">Not Added</string>
<string name="nft_send">Send NFT</string>
<string name="nft_traits_title">Traits</string>
<string name="nft_untitled_collection">Untitled collection</string>
<string name="nft_wallet_count">%1$d NFTs in %2$d collection</string>
<string name="nft_wallet_receive_nft">Tap here to receive first NFT</string>
<string name="nft_wallet_title">NFT collections</string>
@ -1063,7 +1066,7 @@
<string name="transaction_history_transaction_validator">validator: %s</string>
<string name="transfer_min_amount_error">Minimum %s</string>
<string name="transfer_notification_invalid_minimum_transaction_amount_text">The minimum transaction amount is %1$s.</string>
<string name="tron_will_be_send_token_fee_description">Tron network fees for popular tokens are higher. Stake some TRX for cheaper or free transactions.</string>
<string name="tron_will_be_send_token_fee_description">Tron network fees for popular tokens can be higher. Staking TRX may help reduce transaction costs.</string>
<string name="tron_will_be_send_token_fee_title">Save on Tron network fees</string>
<string name="try_to_load_data_again_button_title">Try again</string>
<string name="twin_error_same_card">You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d</string>
@ -1121,7 +1124,7 @@
<string name="visa_onboarding_in_progress_issuer_description">This wont take long. Were completing the activation.</string>
<string name="visa_onboarding_in_progress_title">Getting everything ready!</string>
<string name="visa_onboarding_other_wallet">Other wallet</string>
<string name="visa_onboarding_pin_code_description">Set up a 4-digit code. It will be used for payments.</string>
<string name="visa_onboarding_pin_code_description">Set up a 4-digit code.\nIt will be used for payments.</string>
<string name="visa_onboarding_pin_code_navigation_title">PIN code</string>
<string name="visa_onboarding_pin_code_title">Create PIN Code</string>
<string name="visa_onboarding_pin_not_accepted">PIN was not accepted. Try again or use a different code.</string>
@ -1203,6 +1206,8 @@
<string name="wallet_promo_banner_button_title">Get now with 10% off</string>
<string name="wallet_promo_banner_description">Access 13,000+ cryptocurrencies. Buy, sell, swap, and stake with a single tap.\nLink up to three cards for a backup.</string>
<string name="wallet_promo_banner_title">Discover Tangem Wallet</string>
<string name="wallet_settings_push_notifications_description">Stay notified on wallet incoming transactions and Tangem updates.</string>
<string name="wallet_settings_push_notifications_title">Push Notifications</string>
<string name="wallet_settings_title">Wallet settings</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Use %s or scan a card/ring to unlock access to your wallet</string>
@ -1295,16 +1300,20 @@
<string name="wc_alert_connection_timeout_description">Timeout error. Please, try again later.</string>
<string name="wc_alert_connection_timeout_title">Failed to establish WalletConnect</string>
<string name="wc_alert_domain_issues_description">This domain cannot be verified. Check the request carefully approving.</string>
<string name="wc_alert_session_disconnected_description">Go back to your browser and connect via WalletConnect again.</string>
<string name="wc_alert_session_disconnected_description">Please return to your browser and reconnect via WalletConnect.</string>
<string name="wc_alert_session_disconnected_title">WalletConnect session was disconnected</string>
<string name="wc_alert_unknown_error_description">Error code: %s. If the problem persists — feel free to contact our support.</string>
<string name="wc_alert_unknown_error_title">We\'ve encountered unknown error</string>
<string name="wc_alert_unsupported_method_description">Error code: 8 005. If the problem persists — feel free to contact our support.</string>
<string name="wc_alert_unsupported_method_title">We\'ve encountered unknown error</string>
<string name="wc_alert_unsupported_networks_description">Tangem does not currently support a required network by %s.</string>
<string name="wc_alert_unsupported_networks_title">Unsuported networks</string>
<string name="wc_alert_verified_domain_description">Tangem support a required network by %s.</string>
<string name="wc_alert_verified_domain_title">Verified domain</string>
<string name="wc_alert_wrong_card_description">Wrong card or ring selected in Tangem App</string>
<string name="wc_alert_wrong_card_description">Wrong card or ring selected in the App</string>
<string name="wc_alert_wrong_card_title">We\'ve got some kind of problem</string>
<string name="wc_allow_to_spend">Allow to spend</string>
<string name="wc_common_address">Address</string>
<string name="wc_common_connect">Connect</string>
<string name="wc_common_network">Network</string>
<string name="wc_common_networks">Networks</string>
@ -1319,9 +1328,12 @@
<string name="wc_connections">Connections</string>
<string name="wc_contents">Contents</string>
<string name="wc_copy_data_button_text">Copy data</string>
<string name="wc_custom_allowance_title">Custom allowance</string>
<string name="wc_disconnect_all">Disconnect all</string>
<string name="wc_disconnect_all_alert_desc">Text about discnected all dApps</string>
<string name="wc_disconnect_all_alert_title">Disconect All dApps</string>
<string name="wc_estimated_wallet_changes">Estimated wallet changes</string>
<string name="wc_malicious_transaction">Malicious transaction</string>
<string name="wc_missing_required_network_description">Add the %s network to your profile for this wallet</string>
<string name="wc_missing_required_network_title">The wallet has no required networks</string>
<string name="wc_new_connection">New connection</string>
@ -1331,8 +1343,10 @@
<string name="wc_notification_security_risk_title">Known security risk</string>
<string name="wc_request_from">Request from</string>
<string name="wc_signature_type">Signature Type</string>
<string name="wc_transaction_info_to_title">To</string>
<string name="wc_transaction_request">Transaction request</string>
<string name="wc_transaction_request_title">Transaction request</string>
<string name="wc_unlimited_amount">Unlimited Amount</string>
<string name="wc_wallet_connect">Wallet connect</string>
<string name="welcome_interrupted_backup_alert_discard">Discard</string>
<string name="welcome_interrupted_backup_alert_message">You have an interrupted backup. Do you want to resume?</string>

View file

@ -17,8 +17,9 @@ android {
dependencies {
/** Project - Domain */
implementation(projects.domain.tokens.models)
implementation(projects.domain.appTheme.models)
implementation(projects.domain.models)
implementation(projects.domain.tokens.models)
/** Project - Core */
implementation(projects.core.res)

View file

@ -0,0 +1,309 @@
package com.tangem.core.ui.components.bottomsheets.modal
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.internal.ModalBottomSheetWithBackHandling
import com.tangem.core.ui.components.bottomsheets.internal.collapse
import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero
/**
* Modal bottom sheet with [content], [footer] and optional [title].
*
* Maximum height of sheet is 80% screen height
*
* [Show in Figma](https://www.figma.com/design/09KKG4ZVuFDZhj8WLv5rGJ/%F0%9F%9A%A7-App-experience?node-id=3254-61208&t=vixa7id6ggALcxfF-4)
*/
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> TangemModalBottomSheetWithFooter(
config: TangemBottomSheetConfig,
containerColor: Color = TangemTheme.colors.background.primary,
skipPartiallyExpanded: Boolean = true,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit = {},
crossinline content: @Composable (T) -> Unit,
crossinline footer: @Composable (BoxScope.(T) -> Unit),
) {
val isAlwaysVisible = LocalBottomSheetAlwaysVisible.current
if (isAlwaysVisible) {
PreviewModalBottomSheetWithFooter<T>(
config = config,
containerColor = containerColor,
title = title,
content = content,
footer = footer,
skipPartiallyExpanded = skipPartiallyExpanded,
)
} else {
DefaultModalBottomSheetWithFooter<T>(
config = config,
containerColor = containerColor,
title = title,
content = content,
footer = footer,
onBack = onBack,
skipPartiallyExpanded = skipPartiallyExpanded,
)
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
inline fun <reified T : TangemBottomSheetConfigContent> DefaultModalBottomSheetWithFooter(
config: TangemBottomSheetConfig,
containerColor: Color,
skipPartiallyExpanded: Boolean = true,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit,
crossinline content: @Composable (T) -> Unit,
crossinline footer: @Composable (BoxScope.(T) -> Unit),
) {
var isVisible by remember { mutableStateOf(value = config.isShown) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
if (isVisible && config.content is T) {
BasicModalBottomSheetWithFooter<T>(
config = config,
sheetState = sheetState,
containerColor = containerColor,
title = title,
onBack = onBack,
content = content,
footer = footer,
)
}
LaunchedEffect(key1 = config.isShown) {
if (config.isShown) {
isVisible = true
} else {
sheetState.collapse { isVisible = false }
}
}
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetWithFooter(
config: TangemBottomSheetConfig,
containerColor: Color,
skipPartiallyExpanded: Boolean = true,
crossinline title: @Composable BoxScope.(T) -> Unit,
crossinline content: @Composable (T) -> Unit,
crossinline footer: @Composable BoxScope.(T) -> Unit,
) {
BasicModalBottomSheetWithFooter<T>(
config = config,
sheetState = SheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
initialValue = Expanded,
density = LocalDensity.current,
),
onBack = null,
containerColor = containerColor,
title = title,
content = content,
footer = footer,
)
}
@Suppress("LongParameterList", "LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWithFooter(
config: TangemBottomSheetConfig,
sheetState: SheetState,
containerColor: Color,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit,
crossinline content: @Composable (T) -> Unit,
crossinline footer: @Composable (BoxScope.(T) -> Unit),
modifier: Modifier = Modifier,
) {
val model = config.content as? T ?: return
val bsContent: @Composable ColumnScope.() -> Unit = {
val maxHeight = LocalConfiguration.current.screenHeightDp * MODAL_SHEET_MAX_HEIGHT
val initial = 0
val scrollState = rememberScrollState(initial = initial)
Column(
modifier = Modifier
.systemBarsPadding()
.padding(horizontal = 8.dp, vertical = 8.dp)
.clip(TangemTheme.shapes.roundedCornersLarge)
.background(containerColor)
.heightIn(max = maxHeight.dp)
.fillMaxWidth(),
) {
Box(modifier = Modifier.fillMaxWidth()) {
title(model)
}
// Title bottom shadow(elevation) while content is scrolling
if (scrollState.value != initial) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(2.dp)
.background(
brush = Brush.verticalGradient(
colors = listOf(
TangemTheme.colors.background.secondary.copy(alpha = 0.9F),
Color.Transparent,
),
),
),
)
}
Box(modifier = Modifier.weight(1f, fill = false)) {
Column(
modifier = Modifier
.verticalScroll(state = scrollState)
.padding(bottom = TangemTheme.dimens.spacing80),
) {
content(model)
}
if (scrollState.canScrollForward && scrollState.maxValue != Int.MAX_VALUE) {
BottomFade(
modifier = Modifier.align(Alignment.BottomCenter),
backgroundColor = TangemTheme.colors.background.primary,
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.align(Alignment.BottomCenter),
) {
footer(model)
}
}
}
}
if (onBack != null) {
ModalBottomSheetWithBackHandling(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
onBack = onBack,
dragHandle = null,
content = bsContent,
)
} else {
ModalBottomSheet(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = null,
content = bsContent,
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemModalBottomSheetWithFooter_Preview() {
TangemThemePreview {
TangemModalBottomSheetWithFooter<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
title = { TangemModalBottomSheetTitle(endIconRes = R.drawable.ic_close_24, onEndClick = {}) },
content = {
Column(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.size(56.dp)
.clip(RoundedCornerShape(100))
.background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f))
.padding(12.dp),
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_alert_24),
),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerH24()
Text(
text = "Unsuported networks",
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH8()
Text(
text = "Tangem does not currently support a required network by React App.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH(48.dp)
Text(
text = "Long text to show scrollable content and bottom fade." +
"\nLorem ipsum dolor sit amet, consectetur adipiscing elit. In imperdiet metus non leo " +
"ultricies pulvinar. Pellentesque sed condimentum odio. Sed venenatis ac felis non " +
"consequat. Nunc erat dolor, maximus nec mattis a, tempus at eros. Duis sit amet neque " +
"dui. Donec consectetur nisl id dui convallis, in posuere dolor eleifend. Pellentesque " +
"habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. " +
"Pellentesque consequat scelerisque justo quis tristique. Mauris laoreet venenatis " +
"pharetra. Morbi sed faucibus leo. Praesent elementum pretium posuere. Morbi et felis a " +
"turpis pellentesque rhoncus.",
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
},
footer = {
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = "Go it",
onClick = {},
)
},
)
}
}

View file

@ -4,7 +4,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.getTintForTokenIcon
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.utils.converter.Converter

View file

@ -0,0 +1,23 @@
package com.tangem.core.ui.components.divider
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
@Composable
fun DividerWithPadding(start: Dp = 0.dp, end: Dp = 0.dp, top: Dp = 0.dp, bottom: Dp = 0.dp) {
HorizontalDivider(
modifier = Modifier.padding(
start = start,
end = end,
top = top,
bottom = bottom,
),
thickness = TangemTheme.dimens.size1,
color = TangemTheme.colors.stroke.primary,
)
}

View file

@ -146,7 +146,7 @@ private fun MainContent(
tint = iconTint,
modifier = Modifier
.size(size = iconSize)
.align(alignment = Alignment.CenterVertically),
.align(alignment = Alignment.Top),
)
SpacerW(width = TangemTheme.dimens.spacing10)

View file

@ -5,8 +5,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.core.graphics.toColorInt
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
private const val LIGHT_LUMINANCE = 0.5f
private const val COLOR_HEX_START_INDEX = 2
@ -27,7 +27,7 @@ val CryptoCurrency.networkIconResId: Int
* @return Drawable resource ID for the network.
*/
val Network.iconResId: Int
get() = getActiveIconRes(id.value)
get() = getActiveIconRes(rawId)
/**
* Tries to extract a background color from the contract address of a token.

View file

@ -4,7 +4,7 @@ import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOW
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE_FORMAT_THRESHOLD
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.FORMAT_THRESHOLD
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE
import com.tangem.utils.extensions.isNotWhitespace
import java.math.BigDecimal

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="13dp"
android:height="13dp"
android:viewportWidth="13"
android:viewportHeight="13">
<path
android:pathData="M11.149,0.969C10.775,0.595 10.169,0.595 9.795,0.969L8.567,2.197L10.803,4.433L12.031,3.205C12.405,2.831 12.405,2.225 12.031,1.851L11.149,0.969Z"
android:fillColor="#919191"/>
<path
android:pathData="M9.893,5.342L7.657,3.106L1.047,9.716C0.899,9.865 0.803,10.058 0.775,10.267L0.524,12.153C0.499,12.34 0.659,12.5 0.847,12.476L2.733,12.224C2.941,12.196 3.135,12.101 3.283,11.952L9.893,5.342Z"
android:fillColor="#919191"/>
</vector>

View file

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M9,0.547C8.407,0.547 7.867,0.701 7.283,0.956C6.721,1.201 6.073,1.562 5.271,2.008L2.073,3.788C1.611,4.045 1.214,4.374 0.939,4.825C0.662,5.278 0.547,5.793 0.547,6.357L0.547,6.411C0.546,6.602 0.546,6.825 0.6,7.027C0.733,7.53 1.055,7.86 1.448,8.035C1.663,8.13 1.887,8.175 2.088,8.194V13.66C1.167,14.188 0.547,15.181 0.547,16.318C0.547,16.945 1.055,17.453 1.682,17.453H8.23C8.644,17.453 8.98,17.117 8.98,16.703C8.98,16.289 8.644,15.953 8.23,15.953H2.09C2.254,15.267 2.872,14.757 3.608,14.757H8.23C8.644,14.757 8.98,14.421 8.98,14.007C8.98,13.593 8.644,13.257 8.23,13.257H6.669V8.21H15.572C15.834,8.21 16.205,8.19 16.553,8.035C16.946,7.86 17.268,7.53 17.401,7.027C17.455,6.825 17.454,6.602 17.454,6.411L17.454,6.357C17.454,5.793 17.339,5.278 17.062,4.825C16.787,4.374 16.39,4.045 15.928,3.788L12.73,2.008C11.928,1.562 11.28,1.201 10.718,0.956C10.134,0.701 9.594,0.547 9,0.547ZM5.968,3.337C6.81,2.868 7.395,2.543 7.883,2.33C8.356,2.124 8.685,2.047 9,2.047C9.316,2.047 9.645,2.124 10.118,2.33C10.606,2.543 11.191,2.868 12.033,3.337L15.198,5.098C15.506,5.269 15.679,5.437 15.782,5.606C15.883,5.772 15.954,6 15.954,6.357C15.954,6.489 15.954,6.563 15.951,6.617C15.95,6.635 15.95,6.646 15.949,6.652C15.948,6.657 15.946,6.66 15.945,6.663L15.942,6.664C15.923,6.673 15.884,6.686 15.817,6.695C15.75,6.705 15.67,6.71 15.572,6.71H2.429C2.331,6.71 2.251,6.705 2.184,6.695C2.116,6.686 2.078,6.673 2.059,6.664L2.056,6.663C2.055,6.66 2.053,6.657 2.052,6.652C2.051,6.646 2.051,6.635 2.05,6.617C2.047,6.563 2.047,6.489 2.047,6.357C2.047,6 2.118,5.772 2.219,5.606C2.322,5.437 2.495,5.269 2.802,5.098L5.968,3.337ZM3.608,13.257L3.588,13.257V8.21H5.169V13.257H3.608Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M10.396,15.788C10.103,16.081 10.103,16.556 10.396,16.849C10.689,17.142 11.164,17.142 11.457,16.849L16.849,11.456C17.142,11.163 17.142,10.689 16.849,10.396C16.556,10.103 16.081,10.103 15.788,10.396L10.396,15.788Z"
android:fillColor="#000000"/>
<path
android:pathData="M10.176,11.503C10.176,10.77 10.77,10.176 11.504,10.176C12.237,10.176 12.832,10.77 12.832,11.503C12.832,12.237 12.237,12.831 11.504,12.831C10.77,12.831 10.176,12.237 10.176,11.503ZM11.504,11.676C11.599,11.676 11.676,11.599 11.676,11.503C11.676,11.408 11.599,11.331 11.504,11.331C11.409,11.331 11.332,11.408 11.332,11.503C11.332,11.599 11.409,11.676 11.504,11.676Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M15.741,14.412C15.008,14.412 14.413,15.007 14.413,15.74C14.413,16.473 15.008,17.068 15.741,17.068C16.474,17.068 17.069,16.473 17.069,15.74C17.069,15.007 16.474,14.412 15.741,14.412ZM15.913,15.74C15.913,15.835 15.836,15.912 15.741,15.912C15.646,15.912 15.569,15.835 15.569,15.74C15.569,15.645 15.646,15.568 15.741,15.568C15.836,15.568 15.913,15.645 15.913,15.74Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<group>
<clip-path
android:pathData="M4,0L16,0A4,4 0,0 1,20 4L20,16A4,4 0,0 1,16 20L4,20A4,4 0,0 1,0 16L0,4A4,4 0,0 1,4 0z"/>
<path
android:pathData="M4,0L16,0A4,4 0,0 1,20 4L20,16A4,4 0,0 1,16 20L4,20A4,4 0,0 1,0 16L0,4A4,4 0,0 1,4 0z"
android:fillColor="#EBEBEB"/>
<path
android:pathData="M7.645,7.047C8.365,7.047 8.948,7.631 8.948,8.351C8.948,9.07 8.365,9.654 7.645,9.654C6.925,9.654 6.341,9.07 6.341,8.351C6.341,7.631 6.925,7.047 7.645,7.047Z"
android:strokeAlpha="0.3"
android:fillColor="#919191"
android:fillAlpha="0.3"/>
<path
android:pathData="M9.176,3.268C9.735,2.993 10.397,3.012 10.941,3.326L15.315,5.852C15.895,6.187 16.252,6.806 16.252,7.476V12.527C16.252,13.197 15.895,13.816 15.315,14.151L10.941,16.676C10.397,16.99 9.735,17.01 9.176,16.735L9.066,16.676L4.691,14.151C4.148,13.837 3.799,13.273 3.758,12.651L3.754,12.527V7.476C3.754,6.806 4.111,6.187 4.691,5.852L9.066,3.326L9.176,3.268ZM15.002,10.217C13.239,10.258 11.71,10.72 10.623,11.515C9.502,12.334 8.825,13.521 8.825,15.066H8.778L9.691,15.593L9.765,15.63C9.943,15.703 10.146,15.691 10.316,15.593L14.69,13.068C14.883,12.957 15.002,12.75 15.002,12.527V10.217ZM10.316,4.409C10.122,4.298 9.884,4.298 9.691,4.409L5.316,6.935C5.123,7.046 5.004,7.252 5.004,7.476V12.527L5.01,12.609C5.035,12.8 5.147,12.97 5.316,13.068L7.612,14.393C7.791,12.744 8.627,11.425 9.886,10.505C11.234,9.52 13.039,9.007 15.002,8.966V7.476C15.002,7.252 14.883,7.046 14.69,6.935L10.316,4.409Z"
android:strokeAlpha="0.3"
android:fillColor="#919191"
android:fillType="evenOdd"
android:fillAlpha="0.3"/>
</group>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-12,0a12,12 0,1 1,24 0a12,12 0,1 1,-24 0"
android:strokeAlpha="0.1"
android:fillColor="#0099FF"
android:fillAlpha="0.1"/>
<path
android:pathData="M12.077,6.016C12.519,6.016 12.877,6.374 12.877,6.816V15.253L16.042,12.088C16.355,11.775 16.861,11.775 17.174,12.088C17.486,12.4 17.486,12.907 17.174,13.219L12.643,17.75C12.33,18.062 11.824,18.062 11.511,17.75L6.98,13.219C6.668,12.907 6.668,12.4 6.98,12.088C7.293,11.775 7.799,11.775 8.112,12.088L11.277,15.253V6.816C11.277,6.374 11.635,6.016 12.077,6.016Z"
android:fillColor="#0099FF"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-12,0a12,12 0,1 1,24 0a12,12 0,1 1,-24 0"
android:strokeAlpha="0.1"
android:fillColor="#FF3333"
android:fillAlpha="0.1"/>
<path
android:pathData="M11.511,6.25C11.824,5.938 12.33,5.938 12.643,6.25L17.174,10.781C17.486,11.093 17.486,11.6 17.174,11.912C16.861,12.225 16.355,12.225 16.042,11.912L12.877,8.747V17.184C12.877,17.626 12.519,17.984 12.077,17.984C11.635,17.984 11.277,17.626 11.277,17.184V8.747L8.112,11.912C7.799,12.225 7.293,12.225 6.98,11.912C6.668,11.6 6.668,11.093 6.98,10.781L11.511,6.25Z"
android:fillColor="#FF3333"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M9.992,4.701C8.321,4.701 6.964,6.054 6.964,7.726C6.964,9.397 8.321,10.75 9.992,10.75C11.663,10.75 13.02,9.397 13.02,7.726C13.02,6.054 11.663,4.701 9.992,4.701ZM8.464,7.726C8.464,6.885 9.147,6.201 9.992,6.201C10.837,6.201 11.52,6.885 11.52,7.726C11.52,8.567 10.837,9.25 9.992,9.25C9.147,9.25 8.464,8.567 8.464,7.726Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M6.448,15.067C8.288,13.14 11.709,13.068 13.542,15.058C13.823,15.362 14.298,15.382 14.602,15.101C14.907,14.82 14.926,14.346 14.645,14.041C12.191,11.377 7.766,11.515 5.363,14.031C5.077,14.331 5.088,14.806 5.387,15.092C5.687,15.378 6.162,15.367 6.448,15.067Z"
android:fillColor="#000000"/>
<path
android:pathData="M10.056,0.606H9.942C7.953,0.606 6.384,0.606 5.157,0.771C3.897,0.941 2.886,1.296 2.091,2.092C1.295,2.887 0.94,3.898 0.77,5.158C0.605,6.385 0.605,7.954 0.605,9.943V10.057C0.605,12.046 0.605,13.615 0.77,14.842C0.94,16.102 1.295,17.113 2.091,17.908C2.886,18.704 3.897,19.059 5.157,19.229C6.384,19.394 7.953,19.394 9.942,19.394H10.056C12.045,19.394 13.614,19.394 14.841,19.229C16.101,19.059 17.112,18.704 17.907,17.908C18.703,17.113 19.058,16.102 19.228,14.842C19.393,13.615 19.393,12.046 19.393,10.057V9.943C19.393,7.954 19.393,6.385 19.228,5.158C19.058,3.898 18.703,2.887 17.907,2.092C17.112,1.296 16.101,0.941 14.841,0.771C13.614,0.606 12.045,0.606 10.056,0.606ZM3.152,3.153C3.622,2.682 4.263,2.405 5.357,2.258C6.472,2.108 7.941,2.106 9.999,2.106C12.057,2.106 13.526,2.108 14.641,2.258C15.735,2.405 16.376,2.682 16.846,3.153C17.317,3.623 17.594,4.264 17.741,5.358C17.891,6.473 17.893,7.941 17.893,10C17.893,12.059 17.891,13.527 17.741,14.642C17.594,15.736 17.317,16.377 16.846,16.847C16.376,17.318 15.735,17.595 14.641,17.742C13.526,17.892 12.057,17.894 9.999,17.894C7.941,17.894 6.472,17.892 5.357,17.742C4.263,17.595 3.622,17.318 3.152,16.847C2.681,16.377 2.404,15.736 2.257,14.642C2.107,13.527 2.105,12.059 2.105,10C2.105,7.941 2.107,6.473 2.257,5.358C2.404,4.264 2.681,3.623 3.152,3.153Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

View file

@ -8,6 +8,7 @@ object StringsSigns {
const val DASH_SIGN = ""
const val LOWER_SIGN = "<"
const val TILDE_SIGN = "~"
const val COMA_SIGN = ","
const val INFINITY_SIGN = ""
const val NON_BREAKING_SPACE = '\u00A0'
const val PERCENT = "%"

View file

@ -0,0 +1,11 @@
package com.tangem.utils.extensions
fun <K, V, R> Map<K, V>.mapNotNullValues(transform: (Map.Entry<K, V>) -> R?): Map<K, R> {
return this
.mapNotNull { entry ->
val newValue = transform(entry) ?: return@mapNotNull null
entry.key to newValue
}
.toMap()
}