Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-20 19:50:43 +00:00
commit ae609e7477
155 changed files with 4334 additions and 3342 deletions

View file

@ -26,5 +26,13 @@
{
"name": "alephium",
"version": "5.21.0"
},
{
"name": "scroll",
"version": "undefined"
},
{
"name": "zklink",
"version": "undefined"
}
]

View file

@ -72,14 +72,26 @@ internal class DefaultNetworksStatusesStore(
override suspend fun storeAll(key: UserWalletId, values: Set<NetworkStatus>) {
coroutineScope {
val updatedValues = getSyncOrNull(key).orEmpty()
.addOrReplace(items = values) { prev, new -> prev.network == new.network }
launch { runtimeDataStore.store(key = provideStringKey(key), value = updatedValues) }
launch { storeNetworkStatusInPersistence(userWalletId = key, statuses = updatedValues) }
launch { storeInRuntimeStore(key = key, statuses = values) }
launch { storeInPersistenceStore(userWalletId = key, statuses = values) }
}
}
override suspend fun refresh(key: UserWalletId, networks: Set<Network>) {
val currentStatuses = getSyncOrNull(key).orEmpty()
storeInRuntimeStore(
key = key,
statuses = networks.mapNotNullTo(hashSetOf()) { network ->
val status = currentStatuses.firstOrNull { it.network.id == network.id } ?: return@mapNotNullTo null
status.copy(
value = status.value.copySealed(source = StatusSource.CACHE),
)
},
)
}
/**
* Merge [cachedStatuses] with [runtimeStatuses]
* The resulting set contains statuses from both sets.
@ -108,7 +120,6 @@ internal class DefaultNetworksStatusesStore(
val updatedCachedStatus = when (val status = cached.value) {
is NetworkStatus.NoAccount -> status.copy(source = StatusSource.ONLY_CACHE)
is NetworkStatus.Verified -> status.copy(source = StatusSource.ONLY_CACHE)
is NetworkStatus.Refreshing,
is NetworkStatus.Unreachable,
is NetworkStatus.MissedDerivation,
-> null
@ -121,13 +132,23 @@ internal class DefaultNetworksStatusesStore(
}
}
private suspend fun storeNetworkStatusInPersistence(userWalletId: UserWalletId, statuses: Set<NetworkStatus>) {
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 {
this[userWalletId.stringValue] = newStatuses
val updatedValues = this[userWalletId.stringValue].orEmpty()
.addOrReplace(newStatuses) { prev, new -> prev.networkId == new.networkId }
this[userWalletId.stringValue] = updatedValues
}
}
}

View file

@ -16,4 +16,6 @@ interface NetworksStatusesStore {
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

@ -52,17 +52,30 @@ internal class DefaultQuotesStore(
override suspend fun store(response: QuotesResponse) {
coroutineScope {
launch { storeInRuntimeStore(response = response) }
launch {
storeInRuntimeStore(
values = QuoteConverter(isCached = false).convertSet(input = response.quotes.entries),
)
}
launch { storeInPersistenceStore(response = response) }
}
}
override suspend fun storeEmptyQuotes(currenciesIds: Set<CryptoCurrency.RawID>) {
runtimeStore.update(default = emptySet()) { saved ->
val new = currenciesIds.map { Quote.Empty(it) }
storeInRuntimeStore(values = currenciesIds.map(Quote::Empty).toSet())
}
(saved + new).distinctBy { it.rawCurrencyId }.toSet()
}
override suspend fun refresh(currenciesIds: Set<CryptoCurrency.RawID>) {
val currentStatuses = getSync(currenciesIds)
storeInRuntimeStore(
values = currentStatuses.mapTo(hashSetOf()) { quote ->
when (quote) {
is Quote.Empty -> quote
is Quote.Value -> quote.copy(source = StatusSource.CACHE)
}
},
)
}
private suspend fun getCachedQuotes(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote.Value> {
@ -95,11 +108,9 @@ internal class DefaultQuotesStore(
?: Quote.Empty(currencyId)
}
private suspend fun storeInRuntimeStore(response: QuotesResponse) {
val new = QuoteConverter(isCached = false).convertSet(input = response.quotes.entries)
private suspend fun storeInRuntimeStore(values: Set<Quote>) {
runtimeStore.update(default = emptySet()) { saved ->
(saved + new).distinctBy { it.rawCurrencyId }.toSet()
(saved + values).distinctBy { it.rawCurrencyId }.toSet()
}
}

View file

@ -19,4 +19,6 @@ interface QuotesStore {
/** Store [Quote.Empty] for [currenciesIds] */
suspend fun storeEmptyQuotes(currenciesIds: Set<CryptoCurrency.RawID>)
suspend fun refresh(currenciesIds: Set<CryptoCurrency.RawID>)
}

View file

@ -72,15 +72,26 @@ internal class DefaultStakingBalanceStore(
override suspend fun store(userWalletId: UserWalletId, items: Set<YieldBalanceWrapperDTO>) {
coroutineScope {
launch {
storeInRuntimeStore(
userWalletId = userWalletId,
items = YieldBalanceConverter(isCached = false).convertSet(input = items),
)
updateRuntimeStore(userWalletId = userWalletId) {
YieldBalanceConverter(isCached = false).convertSet(input = items)
}
}
launch { storeInPersistenceStore(userWalletId = userWalletId, items = items) }
}
}
override suspend fun refresh(userWalletId: UserWalletId) {
updateRuntimeStore(userWalletId = userWalletId) { saved ->
saved.mapTo(hashSetOf()) {
when (it) {
is YieldBalance.Data -> it.copy(source = StatusSource.CACHE)
is YieldBalance.Empty -> it.copy(source = StatusSource.CACHE)
is YieldBalance.Error -> it
}
}
}
}
override suspend fun store(
userWalletId: UserWalletId,
integrationId: String,
@ -107,13 +118,16 @@ internal class DefaultStakingBalanceStore(
?.addOrReplace(newBalance) { it.integrationId == integrationId && it.address == address }
?: setOf(newBalance)
storeInRuntimeStore(userWalletId = userWalletId, items = balances)
updateRuntimeStore(userWalletId = userWalletId) { balances }
}
private suspend fun storeInRuntimeStore(userWalletId: UserWalletId, items: Set<YieldBalance>) {
runtimeStore.update(default = emptyMap()) {
it.toMutableMap().apply {
this[userWalletId] = items
private suspend fun updateRuntimeStore(
userWalletId: UserWalletId,
function: (Set<YieldBalance>) -> Set<YieldBalance>,
) {
runtimeStore.update(default = emptyMap()) { saved ->
saved.toMutableMap().apply {
this[userWalletId] = function(this[userWalletId].orEmpty())
}
}
}

View file

@ -26,4 +26,6 @@ interface StakingBalanceStore {
/** Store [item] by [userWalletId], [integrationId] and [address] */
suspend fun store(userWalletId: UserWalletId, integrationId: String, address: String, item: YieldBalanceWrapperDTO)
suspend fun refresh(userWalletId: UserWalletId)
}

View file

@ -6,6 +6,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import javax.annotation.OverridingMethodsMustInvokeSuper
/**
* Abstract class for a component's model.
@ -29,6 +30,7 @@ abstract class Model : InstanceKeeper.Instance {
CoroutineScope(context = dispatchers.mainImmediate + SupervisorJob())
}
@OverridingMethodsMustInvokeSuper
override fun onDestroy() {
runCatching { modelScope.cancel() }
}

View file

@ -10,6 +10,8 @@ android {
}
dependencies {
/* Core */
implementation(projects.core.decompose)
/* Libs - AndroidX */
implementation(deps.lifecycle.runtime.ktx)

View file

@ -1,8 +1,6 @@
package com.tangem.core.deeplink
import android.content.Intent
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
// TODO: Add tests
/**
@ -21,17 +19,11 @@ interface DeepLinksRegistry {
/**
* Registers the given [deepLink].
*
* @see registerWithLifecycle
* @see registerWithViewModel
*/
fun register(deepLink: DeepLink)
/**
* Registers the given [deepLinks].
*
* @see registerWithLifecycle
* @see registerWithViewModel
*/
fun register(deepLinks: Collection<DeepLink>)
@ -50,17 +42,6 @@ interface DeepLinksRegistry {
* */
fun unregisterByIds(ids: Collection<String>)
/**
* Registers the [deepLinks] when the [owner] is resumed and ensures that they are unregistered when the [owner] is
* stopped.
*/
fun registerWithLifecycle(owner: LifecycleOwner, deepLinks: Collection<DeepLink>)
/**
* Registers the [deepLinks] and ensures that they are unregistered when the [ViewModel] is closed.
*/
fun registerWithViewModel(viewModel: ViewModel, deepLinks: Collection<DeepLink>)
/**
* Triggers run last launched [Intent] with deeplink handlers that can handle delayed deeplink
* after handle [Intent] clear that and second time no intent will be handled

View file

@ -3,11 +3,8 @@ package com.tangem.core.deeplink.impl
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import com.tangem.core.deeplink.DeepLink
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.deeplink.utils.DeepLinksLifecycleObserver
import timber.log.Timber
internal class DefaultDeepLinksRegistry : DeepLinksRegistry {
@ -103,19 +100,6 @@ internal class DefaultDeepLinksRegistry : DeepLinksRegistry {
)
}
override fun registerWithLifecycle(owner: LifecycleOwner, deepLinks: Collection<DeepLink>) {
val observer = DeepLinksLifecycleObserver(deepLinksRegistry = this, deepLinks)
owner.lifecycle.addObserver(observer)
}
override fun registerWithViewModel(viewModel: ViewModel, deepLinks: Collection<DeepLink>) {
viewModel.addCloseable {
unregister(deepLinks)
}
register(deepLinks)
}
override fun triggerDelayedDeeplink() {
if (lastIntent != null) {
val intent = lastIntent

View file

@ -0,0 +1,21 @@
package com.tangem.core.deeplink.utils
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.deeplink.DeepLink
import com.tangem.core.deeplink.DeepLinksRegistry
fun AppComponentContext.registerDeepLinks(registry: DeepLinksRegistry, vararg deepLinks: DeepLink) {
registerDeepLinks(registry, deepLinks.toList())
}
fun AppComponentContext.registerDeepLinks(registry: DeepLinksRegistry, deepLinks: Collection<DeepLink>) {
lifecycle.subscribe(
onCreate = {
registry.register(deepLinks)
},
onDestroy = {
registry.unregister(deepLinks)
},
)
}

View file

@ -1,20 +0,0 @@
package com.tangem.core.deeplink.utils
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.tangem.core.deeplink.DeepLink
import com.tangem.core.deeplink.DeepLinksRegistry
internal class DeepLinksLifecycleObserver(
private val deepLinksRegistry: DeepLinksRegistry,
private val deepLinks: Collection<DeepLink>,
) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
deepLinksRegistry.register(deepLinks)
}
override fun onPause(owner: LifecycleOwner) {
deepLinksRegistry.unregister(deepLinks)
}
}

View file

@ -90,6 +90,9 @@ fun getActiveIconRes(blockchainId: String): Int {
"bitrock", "bitrock/test" -> R.drawable.img_bitrock_22
"sonic", "sonic/test" -> R.drawable.img_sonic_22
"apechain", "apechain/test" -> R.drawable.img_apecoin_22
"scroll", "scroll/test" -> R.drawable.ic_alert_24 // FIXME: add icon during full integration
"zklink", "zklink/test" -> R.drawable.img_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22
else -> R.drawable.ic_alert_24
}
}
@ -178,6 +181,9 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"bitrock", "bitrock/test" -> R.drawable.img_bitrock_22
"sonic", "sonic/test" -> R.drawable.img_sonic_22
"apechain", "apechain/test" -> R.drawable.img_apecoin_22
"scroll", "scroll/test" -> R.drawable.ic_alert_24 // FIXME: add icon during full integration
"zklink", "zklink/test" -> R.drawable.img_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22
else -> R.drawable.ic_alert_24
}
}
@ -269,6 +275,9 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"bitrock", "bitrock/test" -> R.drawable.ic_bitrock_22
"sonic", "sonic/test" -> R.drawable.ic_sonic_22
"apechain", "apechain/test" -> R.drawable.ic_apecoin_22
"scroll", "scroll/test" -> R.drawable.ic_alert_24 // FIXME: add icon during full integration
"zklink", "zklink/test" -> R.drawable.ic_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.ic_vanar_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M12.748,18L14.498,14.999L9.25,6H5.749L4,9H7.502L12.748,18ZM16.249,11.999L18,9L16.249,6H12.75L11.001,9H14.5L16.249,11.999Z"
android:fillColor="#000000"/>
</vector>

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M11.217,3.624L11.002,3.5L10.788,3.624L7.502,5.526L7.288,5.65V5.897V9.455L4.214,11.235L4,11.358V11.605V15.41V15.657L4.214,15.78L7.5,17.683L7.714,17.807L7.929,17.683L11,15.905L14.071,17.683L14.286,17.807L14.5,17.683L17.786,15.78L18,15.657V15.41V11.605V11.358L17.786,11.235L14.716,9.457V5.897V5.65L14.503,5.526L11.217,3.624ZM8.145,6.144L11.002,4.49L13.86,6.144V9.454L11.621,10.75C11.456,10.61 11.243,10.525 11.009,10.525C10.773,10.525 10.557,10.612 10.391,10.755L8.145,9.454V6.144ZM10.068,11.561L7.714,10.198L4.857,11.852V15.163L7.714,16.817L10.571,15.163V12.308C10.295,12.164 10.099,11.886 10.068,11.561ZM11.428,12.318V15.163L14.286,16.817L17.143,15.163V11.852L14.288,10.199L11.951,11.552C11.922,11.888 11.717,12.175 11.428,12.318Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M11.002,7.31L14.716,9.46V13.758L11.002,15.908L7.288,13.758V9.46L11.002,7.31ZM8.145,9.954V13.264L11.002,14.918L13.86,13.264V9.954L11.002,8.299L8.145,9.954Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#ffffff"/>
<path
android:pathData="M0,0h22v22h-22z"
android:fillColor="#080A0B"/>
<path
android:pathData="M12.748,18L14.498,14.999L9.25,6H5.749L4,9H7.502L12.748,18ZM16.249,11.999L18,9L16.249,6H12.75L11.001,9H14.5L16.249,11.999Z"
android:fillColor="#03D9AF"/>
</group>
</vector>

View file

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#ffffff"/>
<path
android:pathData="M0,0h22v22h-22z"
android:fillColor="#080A0B"/>
<path
android:pathData="M11.217,3.624L11.002,3.5L10.788,3.624L7.502,5.526L7.288,5.65V5.897V9.455L4.214,11.235L4,11.358V11.605V15.41V15.657L4.214,15.78L7.5,17.683L7.714,17.807L7.929,17.683L11,15.905L14.071,17.683L14.286,17.807L14.5,17.683L17.786,15.78L18,15.657V15.41V11.605V11.358L17.786,11.235L14.716,9.457V5.897V5.65L14.503,5.526L11.217,3.624ZM8.145,6.144L11.002,4.49L13.86,6.144V9.454L11.621,10.75C11.456,10.61 11.243,10.525 11.009,10.525C10.773,10.525 10.557,10.612 10.391,10.755L8.145,9.454V6.144ZM10.068,11.561L7.714,10.198L4.857,11.852V15.163L7.714,16.817L10.571,15.163V12.308C10.295,12.164 10.099,11.886 10.068,11.561ZM11.428,12.318V15.163L14.286,16.817L17.143,15.163V11.852L14.288,10.199L11.951,11.552C11.922,11.888 11.717,12.175 11.428,12.318Z"
android:fillColor="#03D498"
android:fillType="evenOdd"/>
<path
android:pathData="M11.002,7.31L14.716,9.46V13.758L11.002,15.908L7.288,13.758V9.46L11.002,7.31ZM8.145,9.954V13.264L11.002,14.918L13.86,13.264V9.954L11.002,8.299L8.145,9.954Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</group>
</vector>