Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-20 10:25:26 +03:00
commit 0e9ddada09
92 changed files with 2244 additions and 682 deletions

View file

@ -10,7 +10,7 @@ import com.tangem.core.navigation.url.UrlOpener
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -76,7 +76,7 @@ interface ApplicationEntryPoint {
fun getBalanceHidingRepository(): BalanceHidingRepository
fun getUserTokensStore(): UserTokensStore
fun getAppPreferencesStore(): AppPreferencesStore
fun getGetAppThemeModeUseCase(): GetAppThemeModeUseCase

View file

@ -22,7 +22,7 @@ import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.datasource.config.models.Config
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -126,8 +126,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val balanceHidingRepository: BalanceHidingRepository
get() = entryPoint.getBalanceHidingRepository()
private val userTokensStore: UserTokensStore
get() = entryPoint.getUserTokensStore()
private val appPreferencesStore: AppPreferencesStore
get() = entryPoint.getAppPreferencesStore()
val getAppThemeModeUseCase: GetAppThemeModeUseCase
get() = entryPoint.getGetAppThemeModeUseCase()
@ -228,7 +228,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
}
derivationsFinder = DerivationsFinder(
newTokensStore = userTokensStore,
appPreferencesStore = appPreferencesStore,
dispatchers = AppCoroutineDispatcherProvider(),
)
appStateHolder.mainStore = store

View file

@ -158,8 +158,9 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository)
return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository)
}
@Provides

View file

@ -5,7 +5,10 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.local.token.UserTokensStore
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.common.DerivationStyleProvider
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.models.scan.CardDTO
@ -22,7 +25,7 @@ internal data class BlockchainToDerive(
// FIXME: May be move to DI, currently unnecessary
internal class DerivationsFinder(
private val newTokensStore: UserTokensStore,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -64,7 +67,9 @@ internal class DerivationsFinder(
}
private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet<BlockchainToDerive> {
val responseTokens = newTokensStore.getSyncOrNull(userWalletId)
val responseTokens = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
)
?.tokens
?: return hashSetOf()

View file

@ -13,6 +13,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.getBackupCardsCount
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
@ -173,13 +174,7 @@ internal class CardSettingsViewModel @Inject constructor(
userWalletId = userWalletId,
cardId = card.cardId,
isActiveBackupStatus = card.backupStatus?.isActive == true,
backupCardsCount = when (val status = card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount
is CardDTO.BackupStatus.CardLinked,
CardDTO.BackupStatus.NoBackup,
null,
-> 0
},
backupCardsCount = scanResponse.getBackupCardsCount() ?: 0,
),
)
}

View file

@ -18,6 +18,7 @@ dependencies {
implementation(deps.compose.ui.tooling)
implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt)
implementation(deps.compose.coil)
/** Deps */
implementation(deps.kotlin.immutable.collections)

View file

@ -0,0 +1,198 @@
package com.tangem.common.ui.userwallet
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEach
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.common.ui.R
import com.tangem.core.ui.coil.RotationTransformation
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.collections.immutable.persistentListOf
@Composable
fun UserWalletItem(state: UserWalletItemUM, modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
onClick = state.onClick,
enabled = state.isEnabled,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size68)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
CardImage(imageUrl = state.imageUrl)
NameAndInfo(
modifier = Modifier.weight(1f),
name = state.name,
information = state.information,
)
when (state.endIcon) {
UserWalletItemUM.EndIcon.None -> {}
UserWalletItemUM.EndIcon.Arrow -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
UserWalletItemUM.EndIcon.Checkmark -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_check_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
}
}
}
}
}
@Composable
private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) {
Column(
modifier = modifier.heightIn(min = TangemTheme.dimens.size40),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.SpaceEvenly,
) {
Text(
text = name.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
AnimatedContent(
targetState = information.resolveReference(),
label = "User wallet information",
) { information ->
Text(
text = information,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
private fun CardImage(imageUrl: String, modifier: Modifier = Modifier) {
val imageModifier = modifier
.width(TangemTheme.dimens.size24)
.height(TangemTheme.dimens.size36)
.clip(TangemTheme.shapes.roundedCornersSmall)
SubcomposeAsyncImage(
modifier = imageModifier,
model = ImageRequest.Builder(LocalContext.current)
.transformations(RotationTransformation(angle = 90f))
.size(
width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() },
height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() },
)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = {
RectangleShimmer(
modifier = imageModifier,
radius = TangemTheme.dimens.size2,
)
},
error = {
Image(
modifier = imageModifier,
imageVector = ImageVector.vectorResource(R.drawable.img_card_wallet_2_gray_22_36),
contentDescription = null,
)
},
contentDescription = null,
)
}
@Preview
@Composable
private fun Preview() {
TangemThemePreview {
val list = persistentListOf(
UserWalletItemUM(
id = UserWalletId("user_wallet_1".encodeToByteArray()),
name = stringReference("My Wallet"),
information = getInformation(3, "4 496,75 $"),
imageUrl = "",
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_2".encodeToByteArray()),
name = stringReference("Old wallet"),
information = getInformation(3, "4 496,75 $"),
imageUrl = "",
isEnabled = true,
onClick = {},
endIcon = UserWalletItemUM.EndIcon.Arrow,
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = getInformation(3, "4 496,75 $"),
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
),
)
Column {
list.fastForEach { userWalletItemUM ->
UserWalletItem(
modifier = Modifier.fillMaxWidth(),
state = userWalletItemUM,
)
}
}
}
}
private fun getInformation(cardCount: Int, totalBalance: String): TextReference {
val t1 = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
val divider = stringReference(value = "")
val t2 = stringReference(totalBalance)
return TextReference.Combined(wrappedList(t1, divider, t2))
}

View file

@ -0,0 +1,22 @@
package com.tangem.common.ui.userwallet.state
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.wallets.models.UserWalletId
import javax.annotation.concurrent.Immutable
@Immutable
data class UserWalletItemUM(
val id: UserWalletId,
val name: TextReference,
val information: TextReference,
val imageUrl: String,
val isEnabled: Boolean,
val endIcon: EndIcon = EndIcon.None,
val onClick: () -> Unit,
) {
enum class EndIcon {
None,
Arrow,
Checkmark,
}
}

View file

@ -34,7 +34,7 @@ interface StakeKitApi {
@POST("yields/balances")
suspend fun getMultipleYieldBalances(
@Body body: List<YieldBalanceRequestBody>,
): ApiResponse<List<YieldBalanceWrapperDTO>>
): ApiResponse<Set<YieldBalanceWrapperDTO>>
@POST("yields/{integrationId}/balances")
suspend fun getSingleYieldBalance(

View file

@ -1,34 +0,0 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.AppPreferencesUserTokensStore
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.token.UserTokensStoreMigrationRunner
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
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 UserTokensStoreModule {
@Provides
@Singleton
fun provideUserTokensStore(
appPreferencesStore: AppPreferencesStore,
userTokensStoreMigrationRunner: UserTokensStoreMigrationRunner,
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
): UserTokensStore {
return AppPreferencesUserTokensStore(
appPreferencesStore = appPreferencesStore,
userTokensStoreMigrationRunner = userTokensStoreMigrationRunner,
userWalletsStore = userWalletsStore,
dispatchers = dispatchers,
)
}
}

View file

@ -1,63 +0,0 @@
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.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
/**
* Implementation of [UserTokensStore] that based on [appPreferencesStore]
*
* @property appPreferencesStore application preference store
*
[REDACTED_AUTHOR]
*/
internal class AppPreferencesUserTokensStore(
private val appPreferencesStore: AppPreferencesStore,
private val userTokensStoreMigrationRunner: UserTokensStoreMigrationRunner,
private val userWalletsStore: UserWalletsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : UserTokensStore {
init {
runUserTokensMigrations()
}
override fun get(key: UserWalletId): Flow<UserTokensResponse> {
return appPreferencesStore
.getObject<UserTokensResponse>(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue))
.filterNotNull()
}
override suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse? {
return appPreferencesStore.getObjectSyncOrNull(
key = PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue),
)
}
override suspend fun store(key: UserWalletId, value: UserTokensResponse) {
appPreferencesStore.storeObject(
key = PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue),
value = value,
)
}
// TODO: delete in 5.15 (Mobile Sprint 161) [REDACTED_JIRA]
private fun runUserTokensMigrations() {
userWalletsStore.userWallets
.filter { it.isNotEmpty() }
.take(1)
.onEach { userWallets ->
userTokensStoreMigrationRunner.run(ids = userWallets.map { it.walletId.stringValue })
}
.flowOn(dispatchers.io)
.launchIn(CoroutineScope(dispatchers.io))
}
}

View file

@ -3,49 +3,53 @@ package com.tangem.datasource.local.token
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
internal class DefaultStakingBalanceStore(
private val dataStore: StringKeyDataStore<List<YieldBalanceWrapperDTO>>,
private val dataStore: StringKeyDataStore<Set<YieldBalanceWrapperDTO>>,
) : StakingBalanceStore {
override fun get(): Flow<List<YieldBalanceWrapperDTO>> {
return dataStore.get(STAKING_BALANCE_KEY)
private val mutex = Mutex()
override fun get(userWalletId: UserWalletId): Flow<Set<YieldBalanceWrapperDTO>> {
return dataStore.get(userWalletId.stringValue)
}
override suspend fun getSyncOrNull(): List<YieldBalanceWrapperDTO>? {
return dataStore.getSyncOrNull(STAKING_BALANCE_KEY)
override suspend fun getSyncOrNull(userWalletId: UserWalletId): Set<YieldBalanceWrapperDTO>? {
return dataStore.getSyncOrNull(userWalletId.stringValue)
}
override suspend fun store(items: List<YieldBalanceWrapperDTO>) {
return dataStore.store(STAKING_BALANCE_KEY, items)
override suspend fun store(userWalletId: UserWalletId, items: Set<YieldBalanceWrapperDTO>) {
mutex.withLock {
dataStore.store(userWalletId.stringValue, items)
}
}
override fun get(integrationId: String): Flow<List<BalanceDTO>> {
return dataStore.get(STAKING_BALANCE_KEY)
override fun get(userWalletId: UserWalletId, integrationId: String): Flow<List<BalanceDTO>> {
return dataStore.get(userWalletId.stringValue)
.map { balances ->
balances.filter { it.integrationId == integrationId }
.flatMap { it.balances }
}
}
override suspend fun getSyncOrNull(integrationId: String): List<BalanceDTO>? {
return dataStore.getSyncOrNull(STAKING_BALANCE_KEY)
override suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): List<BalanceDTO>? {
return dataStore.getSyncOrNull(userWalletId.stringValue)
?.firstOrNull { it.integrationId == integrationId }?.balances
}
override suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO) {
val balances = dataStore.getSyncOrNull(STAKING_BALANCE_KEY)
?.toMutableList()
?.addOrReplace(item) { item.integrationId == integrationId }
?: listOf(item)
override suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO) {
mutex.withLock {
val balances = dataStore.getSyncOrNull(userWalletId.stringValue)
?.addOrReplace(item) { it.integrationId == integrationId }
?: setOf(item)
return dataStore.store(STAKING_BALANCE_KEY, balances)
}
companion object {
private const val STAKING_BALANCE_KEY = "STAKING_BALANCE_KEY"
dataStore.store(userWalletId.stringValue, balances)
}
}
}

View file

@ -2,19 +2,20 @@ package com.tangem.datasource.local.token
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
interface StakingBalanceStore {
fun get(): Flow<List<YieldBalanceWrapperDTO>>
fun get(userWalletId: UserWalletId): Flow<Set<YieldBalanceWrapperDTO>>
suspend fun getSyncOrNull(): List<YieldBalanceWrapperDTO>?
suspend fun getSyncOrNull(userWalletId: UserWalletId): Set<YieldBalanceWrapperDTO>?
suspend fun store(items: List<YieldBalanceWrapperDTO>)
suspend fun store(userWalletId: UserWalletId, items: Set<YieldBalanceWrapperDTO>)
fun get(integrationId: String): Flow<List<BalanceDTO>>
fun get(userWalletId: UserWalletId, integrationId: String): Flow<List<BalanceDTO>>
suspend fun getSyncOrNull(integrationId: String): List<BalanceDTO>?
suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): List<BalanceDTO>?
suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO)
suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO)
}

View file

@ -1,46 +0,0 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@Deprecated(
message = "Use AppPreferencesStore",
replaceWith = ReplaceWith(
expression = "AppPreferencesStore",
imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"),
),
level = DeprecationLevel.WARNING,
)
interface UserTokensStore {
@Deprecated(
message = "Use getObject",
replaceWith = ReplaceWith(
expression = "appPreferencesStore.getObject(userWalletId)",
imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"),
),
level = DeprecationLevel.WARNING,
)
fun get(key: UserWalletId): Flow<UserTokensResponse>
@Deprecated(
message = "Use getObjectSyncOrNull",
replaceWith = ReplaceWith(
expression = "appPreferencesStore.getObjectSyncOrNull(userWalletId)",
imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"),
),
level = DeprecationLevel.WARNING,
)
suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse?
@Deprecated(
message = "Use storeObject",
replaceWith = ReplaceWith(
expression = "appPreferencesStore.storeObject(userWalletId, response)",
imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"),
),
level = DeprecationLevel.WARNING,
)
suspend fun store(key: UserWalletId, value: UserTokensResponse)
}

View file

@ -1,56 +0,0 @@
package com.tangem.datasource.local.token
import androidx.datastore.core.DataMigration
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.files.FileReader
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.datasource.local.preferences.utils.storeObject
/**
* Migration of saving [UserTokensResponse] from file to [AppPreferencesStore]
*
* @param userWalletId user wallet id
* @param moshi moshi
* @property fileReader file reader
*
[REDACTED_AUTHOR]
*/
internal class UserTokensStoreMigration(
userWalletId: String,
moshi: Moshi,
private val fileReader: FileReader,
) : DataMigration<AppPreferencesStore> {
private val legacyFileName = "user_tokens_$userWalletId"
private val keyName = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId)
@OptIn(ExperimentalStdlibApi::class)
private val adapter = moshi.adapter<UserTokensResponse>()
override suspend fun shouldMigrate(currentData: AppPreferencesStore): Boolean = true
override suspend fun migrate(currentData: AppPreferencesStore): AppPreferencesStore {
val currentKey = currentData.getObjectSyncOrNull<UserTokensResponse>(key = keyName)
if (currentKey != null) return currentData
val value = runCatching {
val json = fileReader.readFile(legacyFileName)
adapter.fromJson(json)
}.getOrNull()
if (value != null) {
currentData.storeObject(key = keyName, value = value)
}
return currentData
}
override suspend fun cleanUp() {
fileReader.removeFile(legacyFileName)
}
}

View file

@ -1,50 +0,0 @@
package com.tangem.datasource.local.token
import com.squareup.moshi.Moshi
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
/**
* Runner that launch migrations of saving user tokens store
*
* @property appPreferencesStore application preference store
* @property fileReader file reader
* @property moshi moshi
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
@Singleton
class UserTokensStoreMigrationRunner @Inject constructor(
private val appPreferencesStore: AppPreferencesStore,
private val fileReader: FileReader,
@NetworkMoshi private val moshi: Moshi,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend fun run(ids: List<String>) {
ids.forEach { id ->
coroutineScope { run(id) }
}
}
private suspend fun run(id: String) {
withContext(dispatchers.io) {
val migration = UserTokensStoreMigration(
userWalletId = id,
moshi = moshi,
fileReader = fileReader,
)
migration.migrate(appPreferencesStore)
migration.cleanUp()
}
}
}

View file

@ -835,6 +835,8 @@
<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, um den Zugriff auf deine Wallet freizuschalten.</string>
<string name="warning_approval_in_progress_message">Die Genehmigung des Swaps ist im Gange und wird in Kürze abgeschlossen sein.</string>
<string name="warning_approval_in_progress_title">Genehmigung in Arbeit</string>
<string name="warning_backup_errors_message">Es scheint, dass die Aktivierung der Karte nicht korrekt abgeschlossen wurde. Dies kann an einem Problem mit dem NFC-Modul deines Gerätes oder an einem falschen Tippen der Karte auf dein Gerät liegen. Bitte wende dich an unser Support-Team, um Unterstützung zu erhalten.</string>
<string name="warning_backup_errors_title">Aktivierungsfehler</string>
<string name="warning_beacon_chain_retirement_content">Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen.</string>
@ -852,8 +854,6 @@
<string name="warning_existential_deposit_title">Netzwerk erfordert eine Mindesteinzahlung</string>
<string name="warning_express_active_transaction_message">Der Swap wird nach Abschluss der Transaktion %s verfügbar sein.</string>
<string name="warning_express_active_transaction_title">Du hast aktive Transaktion</string>
<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_title">Keine Token zum Tauschen verfügbar</string>

View file

@ -823,6 +823,8 @@
<string name="wallet_settings_title">ウォレット設定</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">%sを使用するか、カードをスキャンしてウォレットにアクセスしてください</string>
<string name="warning_approval_in_progress_message">スワップの承認は現在進行中で、まもなく完了する予定です。</string>
<string name="warning_approval_in_progress_title">承認が進行中</string>
<string name="warning_backup_errors_message">カードのアクティベーションが正しく完了しませんでした。デバイスの NFCモジュールに問題があるか、カードをデバイスに正しくタップしていないことが原因かもしれません。サポートチームにお問い合わせください。</string>
<string name="warning_backup_errors_title">アクティベーションに失敗しました</string>
<string name="warning_beacon_chain_retirement_content">BNBネットワーク開発者によると、BEP-2規格のサポートは2024年6月に終了します。この規格の資産を失わないために、BEP-20規格に変換してください。BNBスマートチェーンネットワークへ移行するには、Tangemのスワップサービスをご利用ください。</string>
@ -840,8 +842,6 @@
<string name="warning_existential_deposit_title">ネットワークには最低残高が必要です</string>
<string name="warning_express_active_transaction_message">スワップは、%s の取引完了後に利用可能となります。</string>
<string name="warning_express_active_transaction_title">アクティブな取引があります</string>
<string name="warning_express_approval_in_progress_message">スワップの承認は現在進行中で、まもなく完了する予定です。</string>
<string name="warning_express_approval_in_progress_title">承認が進行中</string>
<string name="warning_express_dust_message">最低のスワップ金額は%1$s です。スワップ後の残金が%2$s を下回らないようにしてください。</string>
<string name="warning_express_no_exchangeable_coins_description">あなたのリストには、交換可能な %s トークンがありません。</string>
<string name="warning_express_no_exchangeable_coins_title">スワップ可能なトークンがありません</string>

View file

@ -859,8 +859,6 @@
<string name="warning_existential_deposit_title">Для работы с сетью необходим депозит</string>
<string name="warning_express_active_transaction_message">Обмен будет доступен после завершения %s транзакции</string>
<string name="warning_express_active_transaction_title">У вас есть активная транзакция</string>
<string name="warning_express_approval_in_progress_message">Разрешение обмена в процессе и будет скоро завершено</string>
<string name="warning_express_approval_in_progress_title">Разрешение в процессе</string>
<string name="warning_express_dust_message">Минимальная сумма обмена - %1$s. Пожалуйста, убедитесь, что остаток после обмена также не будет меньше %2$s.</string>
<string name="warning_express_no_exchangeable_coins_description">У вас в списке нет монет доступных для обмена с %s</string>
<string name="warning_express_no_exchangeable_coins_title">Нет доступных для обмена токенов</string>

View file

@ -844,6 +844,8 @@
<string name="wallet_settings_title">Налаштування гаманця</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Використовуйте %s або відскануйте картку, щоб розблокувати доступ до гаманця</string>
<string name="warning_approval_in_progress_message">Затвердження обміну триває і незабаром буде завершено</string>
<string name="warning_approval_in_progress_title">Затвердження в процесі</string>
<string name="warning_backup_errors_message">Схоже, що активація картки була виконана неправильно. Це може бути пов\'язано з проблемою з модулем NFC вашого пристрою або неправильним прикладанням картки до пристрою. Зверніться за допомогою до нашої служби підтримки.</string>
<string name="warning_backup_errors_title">Помилка активації</string>
<string name="warning_beacon_chain_retirement_content">За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain.</string>
@ -861,8 +863,6 @@
<string name="warning_existential_deposit_title">Для роботи з мережею вимагається депозит</string>
<string name="warning_express_active_transaction_message">Обмін буде доступний після завершення %s транзакції</string>
<string name="warning_express_active_transaction_title">У вас є активна транзакція</string>
<string name="warning_express_approval_in_progress_message">Затвердження обміну триває і незабаром буде завершено</string>
<string name="warning_express_approval_in_progress_title">Затвердження в процесі</string>
<string name="warning_express_dust_message">Мінімальна сума обміну становить - %1$s. Будь ласка, переконайтеся, що залишок на рахунку після обміну буде не менше за %2$s.</string>
<string name="warning_express_no_exchangeable_coins_description">У вашому списку немає доступних монет для обміну %s</string>
<string name="warning_express_no_exchangeable_coins_title">Немає доступних токенів для обміну</string>

View file

@ -1,3 +1,5 @@
import com.android.ide.common.resources.generateLocaleList
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)

View file

@ -0,0 +1,22 @@
package com.tangem.core.ui.coil
import android.graphics.Bitmap
import android.graphics.Matrix
import coil.size.Size
import coil.transform.Transformation
class RotationTransformation(private val angle: Float) : Transformation {
override val cacheKey: String = "rotate:$angle"
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
val matrix = Matrix().apply {
val centerX = input.width / 2f
val centerY = input.height / 2f
postRotate(angle, centerX, centerY)
}
return Bitmap.createBitmap(input, 0, 0, input.width, input.height, matrix, true)
}
}

View file

@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
@ -101,7 +102,11 @@ fun TextShimmer(
* Height and min width will be set automatically
*/
@Composable
fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false) {
fun SmallButtonShimmer(
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(size = TangemTheme.dimens.radius16),
withIcon: Boolean = false,
) {
PrimarySmallButton(
config = SmallButtonConfig(
text = stringReference("B"),
@ -113,7 +118,7 @@ fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false)
},
),
modifier = modifier
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius16))
.clip(shape)
.shimmer(LocalTangemShimmer.current),
)
}

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
@ -29,6 +30,7 @@ class InformationBlockContentScope(val scope: BoxScope) : BoxScope by scope
fun InformationBlock(
title: @Composable BoxScope.() -> Unit,
modifier: Modifier = Modifier,
contentHorizontalPadding: Dp = TangemTheme.dimens.spacing12,
action: (@Composable BoxScope.() -> Unit)? = null,
content: (@Composable InformationBlockContentScope.() -> Unit)? = null,
) {
@ -72,7 +74,7 @@ fun InformationBlock(
if (content != null) {
Box(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing12)
.padding(horizontal = contentHorizontalPadding)
.fillMaxWidth(),
) {
val scope = InformationBlockContentScope(scope = this)

View file

@ -33,6 +33,7 @@ data class SmallButtonConfig(
val text: TextReference,
val onClick: () -> Unit,
val icon: TangemButtonIconPosition = TangemButtonIconPosition.None,
val enabled: Boolean = true,
)
/**
@ -57,6 +58,7 @@ fun SecondarySmallButton(config: SmallButtonConfig, modifier: Modifier = Modifie
SmallButton(config = config, isPrimary = false, modifier = modifier)
}
@Suppress("LongMethod")
@Composable
private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: Modifier = Modifier) {
val shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)
@ -77,7 +79,7 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
color = backgroundColor,
shape = shape,
)
.clickable(enabled = true, onClick = config.onClick)
.clickable(enabled = config.enabled, onClick = config.onClick)
.padding(
paddingValues = when (config.icon) {
is TangemButtonIconPosition.None -> PaddingValues(
@ -100,7 +102,11 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
iconPosition = config.icon,
text = {
val textColor by animateColorAsState(
targetValue = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1,
targetValue = when {
!config.enabled -> TangemTheme.colors.text.disabled
isPrimary -> TangemTheme.colors.text.primary2
else -> TangemTheme.colors.text.primary1
},
label = "Update text color",
)
@ -116,7 +122,11 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = iconResId),
tint = TangemTheme.colors.icon.secondary,
tint = if (config.enabled) {
TangemTheme.colors.icon.secondary
} else {
TangemTheme.colors.icon.inactive
},
contentDescription = null,
)
},
@ -174,5 +184,12 @@ private fun ButtonsSample() {
icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24),
),
)
SecondarySmallButton(
config = config.copy(
text = TextReference.Str(value = "Add token"),
icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24),
enabled = false,
),
)
}
}

View file

@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.*
@ -68,6 +69,7 @@ private class ChildArrowScope(
@Composable
fun ChildArrow(childHeight: Dp, isLastChild: Boolean) {
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
val figureWidth = TangemTheme.dimens.size40
val strokeColor = TangemTheme.colors.stroke.secondary
@ -86,18 +88,31 @@ fun ChildArrow(childHeight: Dp, isLastChild: Boolean) {
)
val arrowHeadRectDp = DpRect(
origin = DpOffset(
x = figureWidth - arrowHeadSize.width,
x = if (isLtr) {
figureWidth - arrowHeadSize.width
} else {
0.dp
},
y = figureRectDp.size.center.y - arrowHeadSize.center.y,
),
size = arrowHeadSize,
)
val curvedArrowRectDp = DpRect(
top = figureRectDp.top,
left = TangemTheme.dimens.size18,
right = figureRectDp.right - arrowHeadRectDp.width,
bottom = figureRectDp.size.center.y,
)
val curvedArrowRectDp = if (isLtr) {
DpRect(
top = figureRectDp.top,
left = TangemTheme.dimens.size18,
right = figureRectDp.right - arrowHeadRectDp.width,
bottom = figureRectDp.size.center.y,
)
} else {
DpRect(
top = figureRectDp.top,
left = arrowHeadRectDp.width,
right = TangemTheme.dimens.size18 + arrowHeadRectDp.width,
bottom = figureRectDp.size.center.y,
)
}
Canvas(
modifier = Modifier
@ -114,20 +129,26 @@ fun ChildArrow(childHeight: Dp, isLastChild: Boolean) {
drawScope = this,
)
scope.drawCurveArrow()
scope.drawArrowHead()
scope.drawCurveArrow(isLtr)
scope.drawArrowHead(isLtr)
if (!isLastChild) {
scope.drawArrowLine()
scope.drawArrowLine(isLtr)
}
}
}
private fun ChildArrowScope.drawArrowHead() {
private fun ChildArrowScope.drawArrowHead(isLtr: Boolean) {
val arrowHeadPath = Path().apply {
moveTo(arrowHeadRect.centerRight)
lineTo(arrowHeadRect.topLeft)
lineTo(arrowHeadRect.bottomLeft)
if (isLtr) {
moveTo(arrowHeadRect.centerRight)
lineTo(arrowHeadRect.topLeft)
lineTo(arrowHeadRect.bottomLeft)
} else {
moveTo(arrowHeadRect.centerLeft)
lineTo(arrowHeadRect.topRight)
lineTo(arrowHeadRect.bottomRight)
}
close()
}
val paint = Paint().apply {
@ -143,13 +164,21 @@ private fun ChildArrowScope.drawArrowHead() {
}
}
private fun ChildArrowScope.drawCurveArrow() {
private fun ChildArrowScope.drawCurveArrow(isLtr: Boolean) {
val curveArrowPath = Path().apply {
moveTo(curvedArrowRect.topLeft)
quadraticBezierTo(
control = curvedArrowRect.bottomLeft,
end = curvedArrowRect.bottomRight,
)
if (isLtr) {
moveTo(curvedArrowRect.topLeft)
quadraticBezierTo(
control = curvedArrowRect.bottomLeft,
end = curvedArrowRect.bottomRight,
)
} else {
moveTo(curvedArrowRect.topRight)
quadraticBezierTo(
control = curvedArrowRect.bottomRight,
end = curvedArrowRect.bottomLeft,
)
}
}
drawPath(
path = curveArrowPath,
@ -158,11 +187,20 @@ private fun ChildArrowScope.drawCurveArrow() {
)
}
private fun ChildArrowScope.drawArrowLine() {
drawLine(
color = strokeColor,
start = curvedArrowRect.topLeft,
end = Offset(curvedArrowRect.left, figureRect.bottom),
strokeWidth = arrowStrokeWidth,
)
private fun ChildArrowScope.drawArrowLine(isLtr: Boolean) {
if (isLtr) {
drawLine(
color = strokeColor,
start = curvedArrowRect.topLeft,
end = Offset(curvedArrowRect.left, figureRect.bottom),
strokeWidth = arrowStrokeWidth,
)
} else {
drawLine(
color = strokeColor,
start = curvedArrowRect.topRight,
end = Offset(curvedArrowRect.right, figureRect.bottom),
strokeWidth = arrowStrokeWidth,
)
}
}

View file

@ -29,8 +29,9 @@ fun BlockchainRow(model: BlockchainRowUM, action: @Composable BoxScope.() -> Uni
modifier = modifier
.heightIn(min = TangemTheme.dimens.size52)
.padding(
vertical = TangemTheme.dimens.spacing8,
horizontal = TangemTheme.dimens.spacing8,
top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing8,
start = TangemTheme.dimens.spacing8,
),
icon = {
RowIcon(
@ -125,7 +126,7 @@ private fun Preview_BlockchainRow(@PreviewParameter(BlockchainRowParameterProvid
BlockchainRow(
model = state,
action = {
TangemSwitch(onCheckedChange = { /* [REDACTED_TODO_COMMENT]*/ }, checked = true)
TangemSwitch(onCheckedChange = { }, checked = true)
},
)
},

View file

@ -6,6 +6,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import com.tangem.core.ui.windowsize.rememberWindowSizePreview
@Composable
@ -14,12 +16,14 @@ fun TangemThemePreview(
typography: TangemTypography = TangemTheme.typography,
dimens: TangemDimens = TangemTheme.dimens,
alwaysShowBottomSheets: Boolean = true,
rtl: Boolean = false,
content: @Composable () -> Unit,
) {
val isDarkTheme = isDark ?: isSystemInDarkTheme()
CompositionLocalProvider(
LocalBottomSheetAlwaysVisible provides alwaysShowBottomSheets,
LocalLayoutDirection provides if (rtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
) {
BoxWithConstraints {
TangemTheme(

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="36dp"
android:viewportWidth="22"
android:viewportHeight="36">
<path
android:fillColor="#C9C9CA"
android:pathData="M22,2L22,34A2,2 0,0 1,20 36L2,36A2,2 0,0 1,0 34L0,2A2,2 0,0 1,2 0L20,0A2,2 0,0 1,22 2z" />
<path
android:fillColor="#A1A1A1"
android:pathData="M10.92,22.14H10.96C11.52,22.14 11.85,21.83 11.85,21.35C11.85,20.84 11.48,20.55 10.95,20.55H10.91C10.36,20.55 10.03,20.89 10.03,21.32C10.03,21.79 10.36,22.14 10.92,22.14ZM9.29,19.89V20.58C9,20.63 8.83,20.85 8.83,21.3C8.83,21.82 9.09,22.11 9.62,22.11H10.05C9.77,21.95 9.51,21.59 9.51,21.16C9.51,20.42 10.06,19.86 10.9,19.86H10.94C11.76,19.86 12.38,20.42 12.38,21.17C12.38,21.65 12.16,21.95 11.87,22.11H12.32V22.79H9.61C8.75,22.78 8.32,22.21 8.32,21.3C8.32,20.39 8.73,19.97 9.29,19.89ZM11.17,7.82V9.23H9.41C9.06,9.23 8.88,9.23 8.75,9.16C8.63,9.1 8.54,9 8.48,8.89C8.41,8.75 8.41,8.58 8.41,8.23V7.82H11.17ZM13.69,8.23V6C13.69,5.65 13.69,5.48 13.62,5.34C13.56,5.22 13.47,5.13 13.35,5.07C13.21,5 13.04,5 12.69,5H12.31V9.23H12.69H12.69C13.04,9.23 13.21,9.23 13.35,9.16C13.47,9.1 13.56,9 13.62,8.89C13.69,8.75 13.69,8.58 13.69,8.23ZM11.17,5V6.41H8.41V6C8.41,5.65 8.41,5.48 8.48,5.34C8.54,5.22 8.63,5.13 8.75,5.07C8.88,5 9.06,5 9.41,5L11.17,5ZM10.21,11.96H11.81V11.57H12.32V11.96H12.96L12.96,12.64H12.32V13.28H11.81V12.64H10.27C10.01,12.64 9.89,12.76 9.89,12.98C9.89,13.11 9.91,13.21 9.95,13.31H9.41C9.37,13.2 9.34,13.05 9.34,12.85C9.34,12.27 9.65,11.96 10.21,11.96ZM12.32,17.44V16.76H9.39V17.44H11.11C11.58,17.44 11.81,17.74 11.81,18.12C11.81,18.53 11.61,18.71 11.17,18.71H9.39L9.39,19.38H11.23C12.04,19.38 12.38,18.97 12.38,18.38C12.38,17.9 12.14,17.58 11.85,17.44H12.32ZM11.88,24.74C11.88,25.16 11.66,25.42 11.16,25.45V23.99C11.61,24.06 11.88,24.33 11.88,24.74ZM10.87,23.29H10.82C9.9,23.29 9.33,23.91 9.33,24.77C9.33,25.52 9.67,26.02 10.29,26.11V25.46C10,25.41 9.84,25.19 9.84,24.79C9.84,24.28 10.15,24 10.7,23.98V26.12H10.9C11.95,26.12 12.38,25.47 12.38,24.74C12.38,23.91 11.77,23.29 10.87,23.29ZM12.32,26.64V27.32H11.87C12.14,27.46 12.38,27.78 12.38,28.21C12.38,28.59 12.22,28.89 11.85,29.04C12.22,29.26 12.38,29.66 12.38,30.03C12.38,30.56 12.05,31 11.24,31H9.39V30.32H11.2C11.63,30.32 11.81,30.14 11.81,29.8C11.81,29.47 11.59,29.16 11.14,29.16H9.39V28.48H11.2C11.63,28.48 11.81,28.29 11.81,27.96C11.81,27.63 11.59,27.32 11.14,27.32H9.39V26.64H12.32ZM10.7,15.52H10.41C10.04,15.52 9.82,15.22 9.82,14.8C9.82,14.47 9.98,14.33 10.23,14.33C10.59,14.33 10.7,14.66 10.7,15.18V15.52ZM11.13,15.16C11.13,14.32 10.88,13.66 10.2,13.66C9.59,13.66 9.33,14.1 9.33,14.64C9.33,15.09 9.5,15.34 9.75,15.53H9.39V16.2H11.31C12.11,16.2 12.38,15.68 12.38,15.03C12.38,14.38 12.09,13.83 11.41,13.78V14.43C11.7,14.47 11.87,14.64 11.87,14.99C11.87,15.39 11.67,15.52 11.28,15.52H11.13V15.16Z" />
</vector>

View file

@ -1,6 +1,7 @@
package com.tangem.data.feedback.converters
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.util.getBackupCardsCount
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
@ -20,10 +21,7 @@ internal object CardInfoConverter : Converter<ScanResponse, CardInfo> {
CardInfo(
userWalletId = createUserWalletId(scanResponse = value),
cardId = card.cardId,
cardsCount = when (val status = value.card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount.toString()
else -> "0"
},
cardsCount = value.getBackupCardsCount()?.toString() ?: "0",
firmwareVersion = card.firmwareVersion.stringValue,
cardBlockchain = walletData?.blockchain,
signedHashesList = card.wallets.map {

View file

@ -45,7 +45,6 @@ import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
@ -261,25 +260,27 @@ internal class DefaultStakingRepository(
override suspend fun fetchSingleYieldBalance(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
cryptoCurrency: CryptoCurrency,
refresh: Boolean,
) = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
val cryptoCurrency = address.cryptoCurrency
val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] ?: return@withContext
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
block = {
val requestBody = getBalanceRequestData(address.address, integrationId)
val requestBody = getBalanceRequestData(address, integrationId)
val result = stakeKitApi.getSingleYieldBalance(
integrationId = requestBody.integrationId,
body = requestBody,
).getOrThrow()
stakingBalanceStore.store(
userWalletId,
requestBody.integrationId,
YieldBalanceWrapperDTO(
balances = result,
@ -292,15 +293,15 @@ internal class DefaultStakingRepository(
override fun getSingleYieldBalanceFlow(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
cryptoCurrency: CryptoCurrency,
): Flow<YieldBalance> = channelFlow {
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalance.Empty)
} else {
launch(dispatchers.io) {
val integrationId = integrationIdMap[address.cryptoCurrency.id.getIntegrationKey()]
val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()]
?: error("Could not get integrationId")
stakingBalanceStore.get(integrationId)
stakingBalanceStore.get(userWalletId, integrationId)
.collectLatest {
send(
yieldBalanceConverter.convert(
@ -316,7 +317,7 @@ internal class DefaultStakingRepository(
withContext(dispatchers.io) {
fetchSingleYieldBalance(
userWalletId,
address,
cryptoCurrency,
)
}
}
@ -324,16 +325,18 @@ internal class DefaultStakingRepository(
override suspend fun getSingleYieldBalanceSync(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
cryptoCurrency: CryptoCurrency,
): YieldBalance = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) {
YieldBalance.Empty
} else {
fetchSingleYieldBalance(userWalletId, address)
fetchSingleYieldBalance(userWalletId, cryptoCurrency)
val integrationId = integrationIdMap[address.cryptoCurrency.id.getIntegrationKey()]
val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()]
?: error("Could not get integrationId")
val result = stakingBalanceStore.getSyncOrNull(integrationId) ?: return@withContext YieldBalance.Error
val result = stakingBalanceStore.getSyncOrNull(userWalletId, integrationId)
?: return@withContext YieldBalance.Error
yieldBalanceConverter.convert(
YieldBalanceConverter.Data(
balance = result,
@ -345,7 +348,7 @@ internal class DefaultStakingRepository(
override suspend fun fetchMultiYieldBalance(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
refresh: Boolean,
) = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
@ -357,23 +360,23 @@ internal class DefaultStakingRepository(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
block = {
val result = stakeKitApi.getMultipleYieldBalances(
addresses
.mapNotNull { networkAddress ->
val cryptoCurrency = networkAddress.cryptoCurrency
val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()]
val availableCurrencies = cryptoCurrencies
.mapNotNull { currency ->
val address = walletManagersFacade.getDefaultAddress(userWalletId, currency.network)
val integrationId = integrationIdMap[currency.id.getIntegrationKey()]
if (integrationId != null) {
networkAddress.address to integrationId
} else {
null
}
if (integrationId != null && address != null) {
address to integrationId
} else {
null
}
.distinct()
.map { getBalanceRequestData(it.first, it.second) },
).getOrThrow()
}
.distinct()
.map { getBalanceRequestData(it.first, it.second) }
.ifEmpty { return@invokeOnExpire }
val result = stakeKitApi.getMultipleYieldBalances(availableCurrencies).getOrThrow()
stakingBalanceStore.store(result)
stakingBalanceStore.store(userWalletId, result)
},
)
} finally {
@ -385,20 +388,20 @@ internal class DefaultStakingRepository(
override fun getMultiYieldBalanceFlow(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
): Flow<YieldBalanceList> = channelFlow {
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalanceList.Empty)
} else {
launch(dispatchers.io) {
stakingBalanceStore.get()
stakingBalanceStore.get(userWalletId)
.collectLatest { send(yieldBalanceListConverter.convert(it)) }
}
withContext(dispatchers.io) {
fetchMultiYieldBalance(
userWalletId,
addresses,
cryptoCurrencies,
)
}
}
@ -406,14 +409,14 @@ internal class DefaultStakingRepository(
override fun getMultiYieldBalanceLce(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
): LceFlow<Throwable, YieldBalanceList> = lceFlow {
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalanceList.Empty)
} else {
launch(dispatchers.io) {
combine(
stakingBalanceStore.get(),
stakingBalanceStore.get(userWalletId),
isYieldBalanceFetching.map { it.getOrElse(userWalletId) { false } },
) { result, isFetching ->
val balances = yieldBalanceListConverter.convert(result)
@ -422,7 +425,7 @@ internal class DefaultStakingRepository(
}
withContext(dispatchers.io) {
catch(
block = { fetchMultiYieldBalance(userWalletId, addresses, refresh = false) },
block = { fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) },
catch = { raise(it) },
)
}
@ -431,13 +434,13 @@ internal class DefaultStakingRepository(
override suspend fun getMultiYieldBalanceSync(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
): YieldBalanceList = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) {
YieldBalanceList.Empty
} else {
fetchMultiYieldBalance(userWalletId, addresses)
val result = stakingBalanceStore.getSyncOrNull() ?: return@withContext YieldBalanceList.Error
fetchMultiYieldBalance(userWalletId, cryptoCurrencies)
val result = stakingBalanceStore.getSyncOrNull(userWalletId) ?: return@withContext YieldBalanceList.Error
yieldBalanceListConverter.convert(result)
}
}

View file

@ -4,13 +4,13 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
import com.tangem.utils.converter.Converter
internal class YieldBalanceListConverter : Converter<List<YieldBalanceWrapperDTO>, YieldBalanceList> {
internal class YieldBalanceListConverter : Converter<Set<YieldBalanceWrapperDTO>, YieldBalanceList> {
internal val converter by lazy(LazyThreadSafetyMode.NONE) {
YieldBalanceConverter()
}
override fun convert(value: List<YieldBalanceWrapperDTO>): YieldBalanceList {
override fun convert(value: Set<YieldBalanceWrapperDTO>): YieldBalanceList {
return if (value.isEmpty()) {
YieldBalanceList.Empty
} else {

View file

@ -8,7 +8,6 @@ import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -28,7 +27,7 @@ internal object TokensDataModule {
fun provideCurrenciesRepository(
tangemTechApi: TangemTechApi,
tangemExpressApi: TangemExpressApi,
userTokensStore: UserTokensStore,
appPreferencesStore: AppPreferencesStore,
userWalletsStore: UserWalletsStore,
walletManagersFacade: WalletManagersFacade,
expressAssetsStore: ExpressAssetsStore,
@ -38,7 +37,7 @@ internal object TokensDataModule {
return DefaultCurrenciesRepository(
tangemTechApi = tangemTechApi,
tangemExpressApi = tangemExpressApi,
userTokensStore = userTokensStore,
appPreferencesStore = appPreferencesStore,
walletManagersFacade = walletManagersFacade,
userWalletsStore = userWalletsStore,
expressAssetsStore = expressAssetsStore,
@ -71,7 +70,7 @@ internal object TokensDataModule {
networksStatusesStore: NetworksStatusesStore,
walletManagersFacade: WalletManagersFacade,
userWalletsStore: UserWalletsStore,
userTokensStore: UserTokensStore,
appPreferencesStore: AppPreferencesStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
): NetworksRepository {
@ -79,7 +78,7 @@ internal object TokensDataModule {
networksStatusesStore = networksStatusesStore,
walletManagersFacade = walletManagersFacade,
userWalletsStore = userWalletsStore,
userTokensStore = userTokensStore,
appPreferencesStore = appPreferencesStore,
cacheRegistry = cacheRegistry,
dispatchers = dispatchers,
)

View file

@ -18,8 +18,12 @@ import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.tangemTech.TangemTechApi
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.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
@ -46,11 +50,11 @@ import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency
internal class DefaultCurrenciesRepository(
private val tangemTechApi: TangemTechApi,
private val tangemExpressApi: TangemExpressApi,
private val userTokensStore: UserTokensStore,
private val userWalletsStore: UserWalletsStore,
private val walletManagersFacade: WalletManagersFacade,
private val expressAssetsStore: ExpressAssetsStore,
private val cacheRegistry: CacheRegistry,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : CurrenciesRepository {
@ -86,7 +90,7 @@ internal class DefaultCurrenciesRepository(
override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
return withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = userTokensStore.getSyncOrNull(userWalletId),
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action" },
)
@ -141,7 +145,7 @@ internal class DefaultCurrenciesRepository(
override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) =
withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = userTokensStore.getSyncOrNull(userWalletId),
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform remove currency action" },
)
@ -163,7 +167,7 @@ internal class DefaultCurrenciesRepository(
override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
return withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = userTokensStore.getSyncOrNull(userWalletId),
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform remove currencies action" },
)
@ -276,9 +280,12 @@ internal class DefaultCurrenciesRepository(
fetchTokensIfCacheExpired(userWallet, refresh)
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
val storedTokens = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWallet.walletId),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.scanResponse)
}
@ -290,9 +297,12 @@ internal class DefaultCurrenciesRepository(
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
val response = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
responseCurrenciesFactory.createCurrency(
currencyId = id,
@ -312,9 +322,12 @@ internal class DefaultCurrenciesRepository(
fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false)
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
val storedTokens = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
val blockchain = Blockchain.fromId(networkId.value)
val blockchainNetworkId = blockchain.toNetworkId()
val coinId = blockchain.toCoinId()
@ -335,7 +348,7 @@ internal class DefaultCurrenciesRepository(
ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
userTokensStore.get(userWalletId)
getSavedUserTokensResponse(userWalletId)
.map { it.group == UserTokensResponse.GroupType.NETWORK }
.collect(::send)
}
@ -347,7 +360,7 @@ internal class DefaultCurrenciesRepository(
ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
userTokensStore.get(userWalletId)
getSavedUserTokensResponse(userWalletId)
.map { it.sort == UserTokensResponse.SortType.BALANCE }
.collect(::send)
}
@ -461,9 +474,14 @@ internal class DefaultCurrenciesRepository(
val userWallet = getUserWallet(userWalletId)
fetchTokensIfCacheExpired(userWallet, refresh = false)
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
val storedTokens = requireNotNull(
value = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue),
),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
return storedTokens.tokens.any {
it.contractAddress != null &&
@ -473,7 +491,7 @@ internal class DefaultCurrenciesRepository(
}
private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow<List<CryptoCurrency>> {
return userTokensStore.get(userWallet.walletId).map { storedTokens ->
return getSavedUserTokensResponse(userWallet.walletId).map { storedTokens ->
responseCurrenciesFactory.createCurrencies(
response = storedTokens,
scanResponse = userWallet.scanResponse,
@ -517,17 +535,26 @@ internal class DefaultCurrenciesRepository(
.let { customTokensMerger.mergeIfPresented(userWalletId, response) }
.let(userTokensBackwardCompatibility::applyCompatibilityAndGetUpdated)
userTokensStore.store(userWallet.walletId, compatibleUserTokensResponse)
appPreferencesStore.storeObject(
key = PreferencesKeys.getUserTokensKey(userWalletId = userWallet.walletId.stringValue),
value = compatibleUserTokensResponse,
)
fetchExchangeableUserMarketCoinsByIds(userWalletId, compatibleUserTokensResponse)
}
private suspend fun checkIsEmptyDemoWallet(userWallet: UserWallet): Boolean {
return demoConfig.isDemoCardId(userWallet.cardId) && userTokensStore.getSyncOrNull(userWallet.walletId) == null
val response = getSavedUserTokensResponseSync(key = userWallet.walletId)
return demoConfig.isDemoCardId(userWallet.cardId) && response == null
}
private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) {
val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response)
userTokensStore.store(userWalletId, compatibleUserTokensResponse)
appPreferencesStore.storeObject(
key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue),
value = compatibleUserTokensResponse,
)
pushTokens(userWalletId, response)
}
@ -561,8 +588,9 @@ internal class DefaultCurrenciesRepository(
private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse {
val userWalletId = userWallet.walletId
val response = userTokensStore.getSyncOrNull(userWalletId)
?: createDefaultUserTokensResponse(userWallet)
val response = appPreferencesStore.getObjectSyncOrNull(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
) ?: createDefaultUserTokensResponse(userWallet)
if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) {
Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId")
@ -622,4 +650,16 @@ internal class DefaultCurrenciesRepository(
}
private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}"
private fun getSavedUserTokensResponse(key: UserWalletId): Flow<UserTokensResponse> {
return appPreferencesStore
.getObject<UserTokensResponse>(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue))
.filterNotNull()
}
private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? {
return appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(key.stringValue),
)
}
}

View file

@ -8,8 +8,11 @@ import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
import com.tangem.data.tokens.utils.NetworkStatusFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.token.UserTokensStore
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.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.core.lce.LceFlow
@ -33,7 +36,7 @@ internal class DefaultNetworksRepository(
private val networksStatusesStore: NetworksStatusesStore,
private val walletManagersFacade: WalletManagersFacade,
private val userWalletsStore: UserWalletsStore,
private val userTokensStore: UserTokensStore,
private val appPreferencesStore: AppPreferencesStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
) : NetworksRepository {
@ -300,9 +303,14 @@ internal class DefaultNetworksRepository(
}
return if (userWallet.isMultiCurrency) {
val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
val response = requireNotNull(
value = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse).asSequence()
} else {

View file

@ -12,6 +12,7 @@ import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.configs.Wallet2CardConfig
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
@ -32,8 +33,6 @@ val UserWallet.cardTypesResolver: CardTypesResolver
get() = scanResponse.cardTypesResolver
fun ScanResponse.twinsIsTwinned(): Boolean = card.isTangemTwins && walletData != null && secondTwinPublicKey != null
fun ScanResponse.supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed
fun ScanResponse.supportsBackup(): Boolean = card.settings.isBackupAllowed
fun ScanResponse.hasDerivation(blockchain: Blockchain, rawDerivationPath: String): Boolean {
return hasDerivation(blockchain, DerivationPath(rawDerivationPath))
@ -66,4 +65,37 @@ private fun ScanResponse.hasDerivation(curve: EllipticCurve, derivationPath: Der
val extendedPublicKeysMap = derivedKeys[foundWallet.publicKey.toMapKey()] ?: return false
val extendedPublicKey = extendedPublicKeysMap[derivationPath]
return extendedPublicKey != null
}
/**
* Get total cards count in wallets set for this [ScanResponse] card
*
* @return null if wallet is not multi-currency or total cards count
*/
fun ScanResponse.getCardsCount(): Int? {
if (!cardTypesResolver.isMultiwalletAllowed()) return null
return when (val status = card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount + 1
is CardDTO.BackupStatus.NoBackup,
is CardDTO.BackupStatus.CardLinked,
null, // Multi-currency wallet without backup function. Example, 4.12
-> 1
}
}
/**
* Get backup cards count for this [ScanResponse] card
*
* @return null if wallet is not multi-currency or total cards count
*/
fun ScanResponse.getBackupCardsCount(): Int? {
return if (cardTypesResolver.isMultiwalletAllowed()) {
when (val status = card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount
else -> 0
}
} else {
null
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.common.util
import com.tangem.domain.wallets.models.UserWallet
/**
* Get total cards count in wallets set for a card that was saved in [UserWallet]
*
* @return null if wallet is not multi-currency or total cards count
*/
fun UserWallet.getCardsCount(): Int? = scanResponse.getCardsCount()
/**
* Get backup cards count for a card that was saved in [UserWallet]
*
* @return null if wallet is not multi-currency or total cards count
*/
fun UserWallet.getBackupCardsCount(): Int? = scanResponse.getBackupCardsCount()

View file

@ -376,15 +376,12 @@ class DefaultWalletManagersFacade(
return walletManagersStore.getAllSync(userWalletId)
}
@Deprecated(
"Use NetworkAddress from CryptoCurrencyStatus",
ReplaceWith("cryptoCurrencyStatus.value.networkAddress"),
)
override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List<Address> {
return getAddresses(userWalletId, network).sortedBy { it.type }
override suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String? {
return getAddresses(userWalletId, network)
.firstOrNull { it.type == AddressType.Default }
?.value
}
@Deprecated("Use NetworkAddress from CryptoCurrencyStatus")
override suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set<Address> {
val manager = getOrCreateWalletManager(
userWalletId = userWalletId,

View file

@ -117,20 +117,18 @@ interface WalletManagersFacade {
suspend fun getStoredWalletManagers(userWalletId: UserWalletId): List<WalletManager>
/**
* Returns ordered list of addresses for selected wallet for given currency
* Returns default network address for selected wallet in given network
*
* @param userWalletId selected wallet id
* @param network network of currency
*/
@Deprecated("Use NetworkAddress from CryptoCurrencyStatus")
suspend fun getAddress(userWalletId: UserWalletId, network: Network): List<Address>
suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String?
/** Returns list of all addresses for all currencies in selected wallet
*
* @param userWalletId selected wallet id
* @param network required to create wallet manager
*/
@Deprecated("Use NetworkAddress from CryptoCurrencyStatus")
suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set<Address>
/**

View file

@ -6,7 +6,7 @@ import arrow.core.raise.either
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
class FetchStakingYieldBalanceUseCase(
@ -16,7 +16,7 @@ class FetchStakingYieldBalanceUseCase(
suspend operator fun invoke(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
cryptoCurrency: CryptoCurrency,
refresh: Boolean = false,
): Either<StakingError, Unit> {
return either {
@ -24,7 +24,7 @@ class FetchStakingYieldBalanceUseCase(
block = {
stakingRepository.fetchSingleYieldBalance(
userWalletId = userWalletId,
address = address,
cryptoCurrency = cryptoCurrency,
refresh = refresh,
)
},

View file

@ -8,7 +8,7 @@ import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
@ -20,11 +20,11 @@ class GetStakingYieldBalanceUseCase(
operator fun invoke(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
cryptoCurrency: CryptoCurrency,
): EitherFlow<StakingError, YieldBalance> {
return stakingRepository.getSingleYieldBalanceFlow(
userWalletId = userWalletId,
address = address,
cryptoCurrency = cryptoCurrency,
).map<YieldBalance, Either<StakingError, YieldBalance>> { it.right() }
.catch { emit(stakingErrorResolver.resolve(it).left()) }
}

View file

@ -15,7 +15,6 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -38,33 +37,33 @@ interface StakingRepository {
suspend fun fetchSingleYieldBalance(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
cryptoCurrency: CryptoCurrency,
refresh: Boolean = false,
)
fun getSingleYieldBalanceFlow(userWalletId: UserWalletId, address: CryptoCurrencyAddress): Flow<YieldBalance>
fun getSingleYieldBalanceFlow(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow<YieldBalance>
suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, address: CryptoCurrencyAddress): YieldBalance
suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): YieldBalance
suspend fun fetchMultiYieldBalance(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
refresh: Boolean = false,
)
fun getMultiYieldBalanceFlow(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
): Flow<YieldBalanceList>
fun getMultiYieldBalanceLce(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
): LceFlow<Throwable, YieldBalanceList>
suspend fun getMultiYieldBalanceSync(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
): YieldBalanceList
suspend fun createAction(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingAction

View file

@ -44,6 +44,7 @@ class FetchCardTokenListUseCase(
val yieldBalances = async {
fetchYieldBalances(
userWalletId = userWalletId,
currencies = currencies,
refresh = refresh,
)
}
@ -77,10 +78,13 @@ class FetchCardTokenListUseCase(
)
}
private suspend fun fetchYieldBalances(userWalletId: UserWalletId, refresh: Boolean) {
val networkAddresses = networksRepository.getNetworkAddresses(userWalletId)
private suspend fun fetchYieldBalances(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
refresh: Boolean,
) {
catch(
block = { stakingRepository.fetchMultiYieldBalance(userWalletId, networkAddresses, refresh) },
block = { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies, refresh) },
catch = { /* Ignore error */ },
)
}

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
@ -29,6 +30,7 @@ class FetchCurrencyStatusUseCase(
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
private val quotesRepository: QuotesRepository,
private val stakingRepository: StakingRepository,
) {
/**
@ -80,8 +82,11 @@ class FetchCurrencyStatusUseCase(
val fetchQuote = async {
fetchQuote(currency.id, refresh)
}
val fetchStakingBalance = async {
fetchStakingBalance(userWalletId, currency, refresh)
}
awaitAll(fetchStatus, fetchQuote)
awaitAll(fetchStatus, fetchQuote, fetchStakingBalance)
}
private suspend fun Raise<CurrencyStatusError>.getCurrency(
@ -122,4 +127,16 @@ class FetchCurrencyStatusUseCase(
raise(CurrencyStatusError.DataError(it))
}
}
private suspend fun Raise<CurrencyStatusError>.fetchStakingBalance(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
refresh: Boolean,
) {
catch(
block = { stakingRepository.fetchSingleYieldBalance(userWalletId, cryptoCurrency, refresh) },
) {
raise(CurrencyStatusError.DataError(it))
}
}
}

View file

@ -69,11 +69,10 @@ internal class CurrenciesStatusesLceOperations(
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
val addresses = networksRepository.getNetworkAddresses(userWalletId)
combine(
getQuotes(currenciesIds),
getNetworksStatuses(userWalletId, networks),
getYieldBalances(userWalletId, addresses),
getYieldBalances(userWalletId, nonEmptyCurrencies),
) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances ->
val statuses = createCurrenciesStatuses(
currencies = nonEmptyCurrencies,
@ -196,11 +195,11 @@ internal class CurrenciesStatusesLceOperations(
private fun getYieldBalances(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
): LceFlow<TokenListError, YieldBalanceList> {
return stakingRepository.getMultiYieldBalanceLce(
userWalletId = userWalletId,
addresses = addresses,
cryptoCurrencies = cryptoCurrencies,
).map { maybeBalances ->
maybeBalances.mapError { TokenListError.DataError(it) }
}

View file

@ -11,7 +11,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
// FIXME: Refactor - [REDACTED_JIRA]
@ -35,7 +34,7 @@ internal class CurrenciesStatusesOperations(
val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right()
val networkStatuses =
networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right()
val yieldBalances = getYieldBalancesSync()
val yieldBalances = getYieldBalancesSync(nonEmptyCurrencies)
return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses, yieldBalances)
},
@ -147,7 +146,7 @@ internal class CurrenciesStatusesOperations(
val currenciesFlow = combine(
getQuotes(currenciesIds),
getNetworksStatuses(networks),
getYieldBalances(),
getYieldBalances(nonEmptyCurrencies),
) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances ->
createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses, maybeYieldBalances)
}
@ -385,25 +384,23 @@ internal class CurrenciesStatusesOperations(
.onEmpty { emit(Error.EmptyNetworksStatuses.left()) }
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun getYieldBalances(): EitherFlow<Error, YieldBalanceList> {
return networksRepository.getNetworkAddressesFlow(userWalletId).flatMapLatest { addresses ->
stakingRepository.getMultiYieldBalanceFlow(
userWalletId = userWalletId,
addresses = addresses,
).map<YieldBalanceList, Either<Error, YieldBalanceList>> { it.right() }
.catch { emit(Error.DataError(it).left()) }
.onEmpty { emit(Error.EmptyYieldBalances.left()) }
}
private fun getYieldBalances(cryptoCurrencies: List<CryptoCurrency>): Flow<Either<Error, YieldBalanceList>> {
return stakingRepository.getMultiYieldBalanceFlow(
userWalletId = userWalletId,
cryptoCurrencies = cryptoCurrencies,
).map<YieldBalanceList, Either<Error, YieldBalanceList>> { it.right() }
.catch { emit(Error.DataError(it).left()) }
.onEmpty { emit(Error.EmptyYieldBalances.left()) }
}
private suspend fun getYieldBalancesSync(): Either<Error.EmptyYieldBalances, YieldBalanceList> {
private suspend fun getYieldBalancesSync(
cryptoCurrencies: List<CryptoCurrency>,
): Either<Error.EmptyYieldBalances, YieldBalanceList> {
return catch(
block = {
val networkAddresses = networksRepository.getNetworkAddresses(userWalletId)
stakingRepository.getMultiYieldBalanceSync(
userWalletId,
networkAddresses,
cryptoCurrencies,
).right()
},
catch = {
@ -417,10 +414,9 @@ internal class CurrenciesStatusesOperations(
): Either<Error.EmptyYieldBalances, YieldBalance> {
return catch(
block = {
val address = networksRepository.getNetworkAddress(userWalletId, cryptoCurrency)
stakingRepository.getSingleYieldBalanceSync(
userWalletId,
address,
cryptoCurrency,
).right()
},
catch = {
@ -429,19 +425,13 @@ internal class CurrenciesStatusesOperations(
)
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun getYieldBalance(cryptoCurrency: CryptoCurrency): EitherFlow<Error, YieldBalance> {
return networksRepository.getNetworkAddressFlow(
userWalletId,
cryptoCurrency,
).flatMapLatest { address ->
stakingRepository.getSingleYieldBalanceFlow(
userWalletId = userWalletId,
address = address,
).map<YieldBalance, Either<Error, YieldBalance>> { it.right() }
.catch { emit(Error.DataError(it).left()) }
.onEmpty { emit(Error.EmptyYieldBalances.left()) }
}
return stakingRepository.getSingleYieldBalanceFlow(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
).map<YieldBalance, Either<Error, YieldBalance>> { it.right() }
.catch { emit(Error.DataError(it).left()) }
.onEmpty { emit(Error.EmptyYieldBalances.left()) }
}
private fun getIds(

View file

@ -16,7 +16,6 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.staking.model.stakekit.transaction.*
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -118,7 +117,7 @@ class MockStakingRepository : StakingRepository {
override suspend fun fetchSingleYieldBalance(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
cryptoCurrency: CryptoCurrency,
refresh: Boolean,
) {
/* no-op */
@ -126,19 +125,19 @@ class MockStakingRepository : StakingRepository {
override fun getSingleYieldBalanceFlow(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
cryptoCurrency: CryptoCurrency,
): Flow<YieldBalance> = channelFlow {
send(YieldBalance.Error)
}
override suspend fun getSingleYieldBalanceSync(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
cryptoCurrency: CryptoCurrency,
): YieldBalance = YieldBalance.Error
override suspend fun fetchMultiYieldBalance(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
refresh: Boolean,
) {
/* no-op */
@ -146,7 +145,7 @@ class MockStakingRepository : StakingRepository {
override fun getMultiYieldBalanceFlow(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
): Flow<YieldBalanceList> = channelFlow {
send(
YieldBalanceList.Data(
@ -157,7 +156,7 @@ class MockStakingRepository : StakingRepository {
override fun getMultiYieldBalanceLce(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
): LceFlow<Throwable, YieldBalanceList> = lceFlow {
send(
YieldBalanceList.Data(
@ -168,7 +167,7 @@ class MockStakingRepository : StakingRepository {
override suspend fun getMultiYieldBalanceSync(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
): YieldBalanceList = YieldBalanceList.Data(
balances = listOf(YieldBalance.Error),
)

View file

@ -25,6 +25,7 @@ dependencies {
implementation(projects.core.navigation)
implementation(projects.core.analytics.models)
implementation(projects.common.routing)
implementation(projects.common.ui)
/* Project - Domain */
implementation(projects.domain.models)

View file

@ -2,6 +2,7 @@ package com.tangem.features.details.component.preview
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -17,7 +18,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
private val previewState = UserWalletListUM(
userWallets = persistentListOf(
UserWalletListUM.UserWalletUM(
UserWalletItemUM(
id = UserWalletId("user_wallet_1".encodeToByteArray()),
name = stringReference("My Wallet"),
information = getInformation(3, "4 496,75 $"),
@ -25,7 +26,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
isEnabled = true,
onClick = {},
),
UserWalletListUM.UserWalletUM(
UserWalletItemUM(
id = UserWalletId("user_wallet_2".encodeToByteArray()),
name = stringReference("Old wallet"),
information = getInformation(3, "4 496,75 $"),
@ -33,7 +34,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
isEnabled = true,
onClick = {},
),
UserWalletListUM.UserWalletUM(
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = getInformation(3, "4 496,75 $"),

View file

@ -1,25 +1,14 @@
package com.tangem.features.details.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal data class UserWalletListUM(
val userWallets: ImmutableList<UserWalletUM>,
val userWallets: ImmutableList<UserWalletItemUM>,
val isWalletSavingInProgress: Boolean,
val addNewWalletText: TextReference,
val onAddNewWalletClick: () -> Unit,
) {
@Immutable
data class UserWalletUM(
val id: UserWalletId,
val name: TextReference,
val information: TextReference,
val imageUrl: String,
val isEnabled: Boolean,
val onClick: () -> Unit,
)
}
)

View file

@ -1,12 +1,12 @@
package com.tangem.features.details.model
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM
import com.tangem.features.details.impl.R
import com.tangem.features.details.utils.UserWalletSaver
import com.tangem.features.details.utils.UserWalletsFetcher
@ -48,7 +48,7 @@ internal class UserWalletListModel @Inject constructor(
}
private fun updateState(
userWallets: ImmutableList<UserWalletUM>,
userWallets: ImmutableList<UserWalletItemUM>,
shouldSaveUserWallets: Boolean,
isWalletSavingInProgress: Boolean,
) = state.update { value ->

View file

@ -1,7 +1,6 @@
package com.tangem.features.details.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
@ -10,32 +9,25 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.impl.R
import com.tangem.features.details.ui.coil.RotationTransformation
@Composable
internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
) {
state.userWallets.forEach { model ->
key(model.id) {
state.userWallets.forEach { state ->
key(state.id) {
UserWalletItem(
modifier = Modifier.fillMaxWidth(),
model = model,
state = state,
)
}
}
@ -47,96 +39,6 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M
}
}
@Composable
private fun UserWalletItem(model: UserWalletListUM.UserWalletUM, modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
onClick = model.onClick,
enabled = model.isEnabled,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size68)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Image(imageUrl = model.imageUrl)
NameAndInfo(
name = model.name,
information = model.information,
)
}
}
}
@Composable
private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) {
Column(
modifier = modifier.heightIn(min = TangemTheme.dimens.size40),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.SpaceEvenly,
) {
Text(
text = name.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
AnimatedContent(
targetState = information.resolveReference(),
label = "User wallet information",
) { information ->
Text(
text = information,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
private fun Image(imageUrl: String, modifier: Modifier = Modifier) {
val imageModifier = modifier
.width(TangemTheme.dimens.size24)
.height(TangemTheme.dimens.size36)
.clip(TangemTheme.shapes.roundedCornersSmall)
SubcomposeAsyncImage(
modifier = imageModifier,
model = ImageRequest.Builder(LocalContext.current)
.transformations(RotationTransformation(angle = 90f))
.size(
width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() },
height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() },
)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = {
RectangleShimmer(
modifier = imageModifier,
radius = TangemTheme.dimens.size2,
)
},
error = {
Image(
modifier = imageModifier,
painter = painterResource(id = R.drawable.img_card_wallet_2_gray_22_36),
contentDescription = null,
)
},
contentDescription = null,
)
}
@Composable
private fun AddWalletButton(
text: TextReference,

View file

@ -1,13 +1,13 @@
package com.tangem.features.details.utils
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM
import com.tangem.features.details.impl.R
import com.tangem.utils.StringsSigns.STARS
import kotlinx.collections.immutable.ImmutableList
@ -19,7 +19,7 @@ internal fun List<UserWallet>.toUiModels(
balances: Map<UserWalletId, TotalFiatBalance> = emptyMap(),
isLoading: Boolean = true,
isBalancesHidden: Boolean = false,
): ImmutableList<UserWalletUM> = this.map { model ->
): ImmutableList<UserWalletItemUM> = this.map { model ->
val balance = balances[model.walletId]
model.toUiModel(
@ -37,7 +37,7 @@ private fun UserWallet.toUiModel(
isLoading: Boolean,
isBalanceHidden: Boolean,
onClick: () -> Unit,
): UserWalletUM = UserWalletUM(
): UserWalletItemUM = UserWalletItemUM(
id = walletId,
name = stringReference(name),
information = getInfo(
@ -59,7 +59,7 @@ private fun UserWallet.getInfo(
): TextReference {
val dividerRef = stringReference(value = "")
val cardCount = getCardCount()
val cardCount = getCardsCount() ?: 1
val cardCountRef = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
@ -99,12 +99,4 @@ private fun getBalanceInfo(
} else {
combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN))
}
}
private fun UserWallet.getCardCount() = when (val status = scanResponse.card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount.inc()
is CardDTO.BackupStatus.CardLinked -> status.cardCount.inc()
is CardDTO.BackupStatus.NoBackup,
null,
-> 1
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.details.utils
import arrow.core.Either
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
@ -22,7 +23,6 @@ import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM
import com.tangem.features.details.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -40,7 +40,7 @@ internal class UserWalletsFetcher @Inject constructor(
) {
@OptIn(ExperimentalCoroutinesApi::class)
val userWallets: Flow<ImmutableList<UserWalletUM>> = getWalletsUseCase().transformLatest { wallets ->
val userWallets: Flow<ImmutableList<UserWalletItemUM>> = getWalletsUseCase().transformLatest { wallets ->
emit(wallets.toUiModels(onClick = ::navigateToWalletSettings))
combine(
@ -72,7 +72,7 @@ internal class UserWalletsFetcher @Inject constructor(
maybeAppCurrency: Either<SelectedAppCurrencyError, AppCurrency>,
maybeBalances: Lce<TokenListError, Map<UserWalletId, TotalFiatBalance>>,
balanceHidingSettings: BalanceHidingSettings,
): Lce<Error, ImmutableList<UserWalletUM>> = lce {
): Lce<Error, ImmutableList<UserWalletItemUM>> = lce {
val balances = withError(
transform = { Error.UnableToGetBalances },
block = { maybeBalances.bindOrNull().orEmpty() },

View file

@ -286,6 +286,7 @@ private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Mod
isLastItem = index == currentItems.lastIndex,
content = {
BlockchainRow(
modifier = Modifier.padding(end = TangemTheme.dimens.spacing8),
model = with(network) {
BlockchainRowUM(
name = name,

View file

@ -20,6 +20,7 @@ dependencies {
implementation(projects.domain.markets)
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.wallets.models)
/* Compose */
implementation(deps.compose.coil)
@ -46,6 +47,7 @@ dependencies {
implementation(projects.core.ui)
implementation(projects.core.featuretoggles)
/* Common */
implementation(projects.common.ui)
implementation(projects.common.uiCharts)
}

View file

@ -6,24 +6,47 @@ import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.markets.component.BottomSheetState
import com.tangem.features.markets.details.api.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel
import com.tangem.features.markets.details.impl.model.state.TokenNetworksState
import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@Stable
internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: MarketsTokenDetailsComponent.Params,
@Assisted private val onBack: () -> Unit,
portfolioComponentFactory: MarketsPortfolioComponent.Factory,
) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent {
private val model: MarketsTokenDetailsModel = getOrCreateModel(params)
private val portfolioComponent = portfolioComponentFactory.create(
context = child("my_portfolio"),
params = MarketsPortfolioComponent.Params(params.token.id),
)
init {
componentScope.launch {
model.networksState.collectLatest {
when (it) {
is TokenNetworksState.NetworksAvailable -> portfolioComponent.setTokenNetworks(it.networks)
TokenNetworksState.NoNetworksAvailable -> portfolioComponent.setNoNetworksAvailable()
else -> {}
}
}
}
}
@Composable
override fun BottomSheetContent(
bottomSheetState: State<BottomSheetState>,
@ -48,6 +71,9 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
state = state,
onBackClick = onBack,
onHeaderSizeChange = onHeaderSizeChange,
portfolioBlock = { modifier ->
portfolioComponent.Content(modifier)
},
modifier = modifier,
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.markets.details.impl.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.ui.charts.state.*
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
@ -25,6 +26,7 @@ import com.tangem.features.markets.details.impl.model.formatter.*
import com.tangem.features.markets.details.impl.model.formatter.formatAsPrice
import com.tangem.features.markets.details.impl.model.formatter.getChangePercentBetween
import com.tangem.features.markets.details.impl.model.formatter.getPercentByInterval
import com.tangem.features.markets.details.impl.model.state.TokenNetworksState
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.impl.R
@ -43,6 +45,7 @@ import javax.inject.Inject
@Suppress("LargeClass", "LongParameterList")
@Stable
@ComponentScoped
internal class MarketsTokenDetailsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
@ -117,6 +120,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED)
val isVisibleOnScreen = MutableStateFlow(false)
val networksState = MutableStateFlow<TokenNetworksState>(TokenNetworksState.Loading)
val state = MutableStateFlow(
MarketsTokenDetailsUM(
@ -292,6 +296,14 @@ internal class MarketsTokenDetailsModel @Inject constructor(
)
}
val networks = result.networks
networksState.value = if (networks.isNullOrEmpty()) {
TokenNetworksState.NoNetworksAvailable
} else {
TokenNetworksState.NetworksAvailable(networks)
}
chartDataProducer.runTransaction {
updateLook {
it.copy(

View file

@ -0,0 +1,12 @@
package com.tangem.features.markets.details.impl.model.state
import com.tangem.domain.markets.TokenMarketInfo
internal sealed class TokenNetworksState {
data object Loading : TokenNetworksState()
data object NoNetworksAvailable : TokenNetworksState()
data class NetworksAvailable(val networks: List<TokenMarketInfo.Network>) : TokenNetworksState()
}

View file

@ -51,6 +51,7 @@ internal fun MarketsTokenDetailsContent(
state: MarketsTokenDetailsUM,
onBackClick: () -> Unit,
onHeaderSizeChange: (Dp) -> Unit,
portfolioBlock: @Composable (Modifier) -> Unit,
modifier: Modifier = Modifier,
) {
Content(
@ -58,6 +59,7 @@ internal fun MarketsTokenDetailsContent(
state = state,
onBackClick = onBackClick,
onHeaderSizeChange = onHeaderSizeChange,
portfolioBlock = portfolioBlock,
)
InfoBottomSheet(config = state.infoBottomSheet)
@ -69,6 +71,7 @@ private fun Content(
state: MarketsTokenDetailsUM,
onBackClick: () -> Unit,
onHeaderSizeChange: (Dp) -> Unit,
portfolioBlock: @Composable (Modifier) -> Unit,
modifier: Modifier = Modifier,
) {
val backgroundColor = LocalMainBottomSheetColor.current.value
@ -129,6 +132,7 @@ private fun Content(
tokenMarketDetailsBody(
state = state.body,
portfolioBlock = portfolioBlock,
)
}
}
@ -288,6 +292,7 @@ private fun Preview() {
),
onHeaderSizeChange = {},
onBackClick = {},
portfolioBlock = {},
)
}
}

View file

@ -11,16 +11,31 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
internal fun LazyListScope.tokenMarketDetailsBody(state: MarketsTokenDetailsUM.Body) {
internal fun LazyListScope.tokenMarketDetailsBody(
state: MarketsTokenDetailsUM.Body,
portfolioBlock: @Composable (Modifier) -> Unit,
) {
when (state) {
MarketsTokenDetailsUM.Body.Loading -> {
loading()
item("description-loading") {
DescriptionPlaceholder(modifier = Modifier.blockPaddings())
}
item(key = "portfolio") {
portfolioBlock(Modifier.blockPaddings())
}
loadingInfoBlocks()
}
is MarketsTokenDetailsUM.Body.Content -> {
if (state.description != null) {
description(state.description)
}
item(key = "portfolio") {
portfolioBlock(Modifier.blockPaddings())
}
infoBlocksList(state.infoBlocks)
}
is MarketsTokenDetailsUM.Body.Error -> {
@ -106,11 +121,7 @@ internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.Informati
}
}
private fun LazyListScope.loading() {
item("description-loading") {
DescriptionPlaceholder(modifier = Modifier.blockPaddings())
}
private fun LazyListScope.loadingInfoBlocks() {
item("insights-loading") {
InsightsBlockPlaceholder(modifier = Modifier.blockPaddings())
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.markets.portfolio.api
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.markets.TokenMarketInfo
import kotlinx.serialization.Serializable
@Stable
interface MarketsPortfolioComponent : ComposableContentComponent {
@Serializable
data class Params(val tokenId: String)
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>)
fun setNoNetworksAvailable()
interface Factory : ComponentFactory<Params, MarketsPortfolioComponent>
}

View file

@ -0,0 +1,43 @@
package com.tangem.features.markets.portfolio.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel
import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Stable
internal class DefaultMarketsPortfolioComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: MarketsPortfolioComponent.Params,
) : AppComponentContext by context, MarketsPortfolioComponent {
private val model: MarketsPortfolioModel = getOrCreateModel(params)
override fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) = model.setTokenNetworks(networks)
override fun setNoNetworksAvailable() = model.setNoNetworksAvailable()
@Composable
override fun Content(modifier: Modifier) {
MyPortfolio(
modifier = modifier,
state = MyPortfolioUM.Loading,
)
}
@AssistedFactory
interface Factory : MarketsPortfolioComponent.Factory {
override fun create(
context: AppComponentContext,
params: MarketsPortfolioComponent.Params,
): DefaultMarketsPortfolioComponent
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.markets.portfolio.impl.di
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.markets.portfolio.impl.DefaultMarketsPortfolioComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ComponentModule {
@Binds
@Singleton
fun bindMarketsPortfolioComponent(
factory: DefaultMarketsPortfolioComponent.Factory,
): MarketsPortfolioComponent.Factory
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.markets.portfolio.impl.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
internal interface ModelModule {
@Binds
@IntoMap
@ClassKey(MarketsPortfolioModel::class)
fun provideMarketsPortfolioModel(model: MarketsPortfolioModel): Model
}

View file

@ -0,0 +1,30 @@
package com.tangem.features.markets.portfolio.impl.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import javax.inject.Inject
@Stable
@ComponentScoped
internal class MarketsPortfolioModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
@Suppress("UnusedPrivateMember")
private val params = paramsContainer.require<MarketsPortfolioComponent.Params>()
@Suppress("UnusedPrivateMember")
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) {
// TODO [REDACTED_TASK_KEY]
}
fun setNoNetworksAvailable() {
// TODO [REDACTED_TASK_KEY]
}
}

View file

@ -0,0 +1,240 @@
package com.tangem.features.markets.portfolio.impl.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.SpacerW6
import com.tangem.core.ui.components.TangemSwitch
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.currency.icon.CoinIcon
import com.tangem.core.ui.components.rows.ArrowRow
import com.tangem.core.ui.components.rows.BlockchainRow
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider
import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM
import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM
@Composable
internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<AddToPortfolioBSContentUM>(
config = config,
containerColor = TangemTheme.colors.background.tertiary,
titleText = resourceReference(R.string.markets_add_to_portfolio_button),
) {
Content(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = TangemTheme.dimens.spacing16),
state = config.content as AddToPortfolioBSContentUM,
)
}
}
@Composable
private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
UserWalletItem(state.selectedWallet)
NetworkSelection(
modifier = Modifier.fillMaxWidth(),
state = state.selectNetworkUM,
)
AnimatedVisibility(
visible = state.isScanCardNotificationVisible,
modifier = Modifier.fillMaxWidth(),
) {
ScanWalletWarning(modifier = Modifier.fillMaxWidth())
}
PrimaryButtonIconEnd(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.common_continue),
iconResId = R.drawable.ic_tangem_24,
onClick = {},
)
}
}
@Suppress("LongMethod")
@Composable
private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier,
title = {
Text(
text = stringResource(R.string.markets_select_network),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState()),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens.spacing14),
verticalAlignment = Alignment.CenterVertically,
) {
CoinIcon(
modifier = Modifier.size(TangemTheme.dimens.size36),
url = state.iconUrl,
alpha = 1f,
colorFilter = null,
fallbackResId = R.drawable.ic_custom_token_44,
)
SpacerW12()
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.alignByBaseline(),
text = state.tokenName,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
SpacerW6()
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.alignByBaseline(),
text = state.tokenCurrencySymbol,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
)
}
state.networks.fastForEachIndexed { index, network ->
ArrowRow(
isLastItem = index == state.networks.lastIndex,
content = {
BlockchainRow(
modifier = Modifier.padding(
end = TangemTheme.dimens.spacing4,
),
model = with(network) {
BlockchainRowUM(
name = name,
type = type,
iconResId = iconResId,
isMainNetwork = isMainNetwork,
isSelected = isSelected,
)
},
action = {
TangemSwitch(
checked = network.isSelected,
onCheckedChange = {
state.onNetworkSwitchClick(network, it)
},
)
},
)
},
)
}
}
}
}
@Composable
private fun ScanWalletWarning(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.background(
color = TangemTheme.colors.button.disabled,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10),
) {
Icon(
modifier = Modifier.requiredSize(TangemTheme.dimens.size20),
imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24),
contentDescription = null,
)
Text(
text = stringResource(R.string.markets_generate_addresses_notification),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview(
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
) {
TangemThemePreview {
AddToPortfolioBottomSheet(
config = TangemBottomSheetConfig(
isShow = true,
content = content,
onDismissRequest = {},
),
)
}
}
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun PreviewContent(
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
) {
TangemThemePreview {
Content(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.fillMaxWidth(),
state = content,
)
}
}
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun PreviewContentRtl(
@PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM,
) {
TangemThemePreview(rtl = true) {
Content(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.fillMaxWidth(),
state = content,
)
}
}

View file

@ -0,0 +1,174 @@
package com.tangem.features.markets.portfolio.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SmallButtonShimmer
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
@Composable
internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier,
contentHorizontalPadding = 0.dp,
title = {
Text(
text = stringResource(R.string.markets_common_my_portfolio),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
},
action = {
if (state !is MyPortfolioUM.Tokens) return@InformationBlock
when (state.buttonState) {
MyPortfolioUM.Tokens.AddButtonState.Loading -> {
SmallButtonShimmer(
modifier = Modifier.size(width = 63.dp, height = TangemTheme.dimens.size18),
shape = RoundedCornerShape(TangemTheme.dimens.radius3),
)
}
else -> {
SecondarySmallButton(
config = SmallButtonConfig(
text = resourceReference(R.string.markets_add_token),
icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24),
onClick = state.onAddClick,
enabled = state.buttonState == MyPortfolioUM.Tokens.AddButtonState.Available,
),
)
}
}
},
) {
when (state) {
is MyPortfolioUM.Tokens -> TokenList(state = state)
is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state)
MyPortfolioUM.Loading -> LoadingPlaceholder()
MyPortfolioUM.Unavailable -> UnavailableContent()
}
}
}
@Composable
private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier) {
Column(modifier) {
state.tokens.fastForEachIndexed { index, token ->
PortfolioItem(
state = token,
lastInList = index == state.tokens.size - 1,
)
}
}
}
@Composable
private fun UnavailableContent(modifier: Modifier = Modifier) {
Text(
modifier = modifier
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing12,
),
text = stringResource(R.string.markets_add_to_my_portfolio_unavailable_description),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
@Composable
private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing12,
),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Text(
text = "To start buying, exchanging or receiving this asset, add this token to at least 1 network",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.markets_add_to_portfolio_button),
onClick = state.onAddClick,
)
}
}
@Composable
private fun LoadingPlaceholder(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing12,
),
) {
TextShimmer(
modifier = Modifier.fillMaxWidth(),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
TextShimmer(
modifier = Modifier.fillMaxWidth(fraction = 0.7f),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) {
TangemThemePreview {
Box(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.padding(TangemTheme.dimens.spacing8),
) {
MyPortfolio(state)
}
}
}
@Preview
@Composable
private fun PreviewRtl(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) {
TangemThemePreview(rtl = true) {
Box(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.padding(TangemTheme.dimens.spacing8),
) {
MyPortfolio(state)
}
}
}

View file

@ -0,0 +1,292 @@
package com.tangem.features.markets.portfolio.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.currency.icon.CoinIcon
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.LocalHapticManager
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider
import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM
import com.tangem.utils.StringsSigns
// TODO add rest of the balance states ([REDACTED_TASK_KEY] [Markets] Portfolio token item UI Improvement)
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifier: Modifier = Modifier) {
val hapticManager = LocalHapticManager.current
Column(modifier) {
Row(
modifier = Modifier
.combinedClickable(
onClick = {
hapticManager.perform(TangemHapticEffect.View.ContextClick)
state.onClick()
},
onLongClick = {
hapticManager.perform(TangemHapticEffect.View.LongPress)
state.onLongTap()
},
)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.padding(
vertical = TangemTheme.dimens.spacing15,
horizontal = TangemTheme.dimens.spacing12,
),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
) {
Content(state)
}
PortfolioQuickActions(
modifier = Modifier.padding(
bottom = if (lastInList) {
TangemTheme.dimens.spacing12
} else {
TangemTheme.dimens.spacing24
},
),
isVisible = state.isQuickActionsShown,
onActionClick = state.onQuickActionClick,
)
}
}
@Composable
private fun RowScope.Content(state: PortfolioTokenUM) {
// TODO add custom token
CoinIcon(
modifier = Modifier.size(TangemTheme.dimens.size36),
url = state.iconUrl,
alpha = 1f, // TODO add disabled state
colorFilter = null,
fallbackResId = R.drawable.ic_custom_token_44,
)
Column(
modifier = Modifier.align(Alignment.CenterVertically),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
) {
when (state.balanceContent) {
is PortfolioTokenUM.BalanceContent.Disabled -> {
Disabled(
state = state,
disabledText = state.balanceContent.text.resolveReference(),
)
}
PortfolioTokenUM.BalanceContent.Loading -> {
Loading(state = state)
}
is PortfolioTokenUM.BalanceContent.TokenBalance -> {
TokenBalance(
state = state,
content = state.balanceContent,
)
}
}
}
}
@Composable
private fun ColumnScope.TokenBalance(state: PortfolioTokenUM, content: PortfolioTokenUM.BalanceContent.TokenBalance) {
val balance = if (content.hidden) {
StringsSigns.STARS
} else {
content.balance
}
val tokenAmount = if (content.hidden) {
StringsSigns.STARS
} else {
content.tokenAmount
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
modifier = Modifier.alignByBaseline(),
text = state.title,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier.alignByBaseline(),
text = balance,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = state.subtitle,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
Text(
text = tokenAmount,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
private fun ColumnScope.Loading(state: PortfolioTokenUM) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
modifier = Modifier.alignByBaseline(),
text = state.title,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
TextShimmer(
modifier = Modifier
.width(TangemTheme.dimens.size40)
.alignByBaseline(),
style = TangemTheme.typography.body2,
textSizeHeight = true,
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = state.subtitle,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
TextShimmer(
modifier = Modifier
.width(TangemTheme.dimens.size40)
.alignByBaseline(),
style = TangemTheme.typography.caption2,
textSizeHeight = true,
)
}
}
@Composable
private fun Disabled(state: PortfolioTokenUM, disabledText: String, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
Modifier.weight(1f),
) {
Text(
text = state.title,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = state.subtitle,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
Text(
text = disabledText,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
var quickActionsShown by remember { mutableStateOf(false) }
var quickActionsShown2 by remember { mutableStateOf(false) }
val sampleToken = PreviewMyPortfolioUMProvider().sampleToken
Box(
Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
) {
Column {
PortfolioItem(
state = sampleToken
.copy(
onClick = {
if (quickActionsShown2) {
quickActionsShown2 = false
}
quickActionsShown = quickActionsShown.not()
},
isQuickActionsShown = quickActionsShown,
),
lastInList = true,
)
PortfolioItem(
state = sampleToken
.copy(
onClick = {
if (quickActionsShown) {
quickActionsShown = false
}
quickActionsShown2 = quickActionsShown2.not()
},
isQuickActionsShown = quickActionsShown2,
),
lastInList = true,
)
PortfolioItem(
state = sampleToken
.copy(
balanceContent = (
sampleToken.balanceContent
as PortfolioTokenUM.BalanceContent.TokenBalance
)
.copy(hidden = true),
),
lastInList = true,
)
PortfolioItem(
state = sampleToken
.copy(
balanceContent = PortfolioTokenUM.BalanceContent.Disabled(
stringReference("No Address"),
),
),
lastInList = true,
)
PortfolioItem(
state = sampleToken
.copy(balanceContent = PortfolioTokenUM.BalanceContent.Loading),
lastInList = true,
)
}
}
}
}

View file

@ -0,0 +1,214 @@
package com.tangem.features.markets.portfolio.impl.ui
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.LocalHapticManager
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM
@Composable
internal fun PortfolioQuickActions(
isVisible: Boolean,
onActionClick: (QuickActionUM) -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
visible = isVisible,
enter = expandVertically(expandFrom = Alignment.Top),
exit = shrinkVertically(shrinkTowards = Alignment.Top),
) {
Column(modifier = modifier) {
LineSeparator()
QuickActionItem(
state = QuickActionUM.Buy,
onClick = { onActionClick(QuickActionUM.Buy) },
)
LineSeparator()
QuickActionItem(
state = QuickActionUM.Exchange,
onClick = { onActionClick(QuickActionUM.Exchange) },
)
LineSeparator()
QuickActionItem(
state = QuickActionUM.Receive,
onClick = { onActionClick(QuickActionUM.Receive) },
)
}
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) {
val lineColor = TangemTheme.colors.stroke.primary
val strokeWidth = TangemTheme.dimens.size1
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
val verticalPadding = TangemTheme.dimens.spacing2
val startPadding = TangemTheme.dimens.spacing28
val height = TangemTheme.dimens.size16 + verticalPadding * 2
Canvas(
modifier = modifier
.animateEnterExit(
enter = expandVertically(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
),
expandFrom = Alignment.Top,
) + fadeIn(),
exit = shrinkVertically(
spring(
stiffness = Spring.StiffnessLow,
),
shrinkTowards = Alignment.Top,
) + fadeOut(),
)
.fillMaxWidth()
.height(height),
) {
val x = if (isLtr) startPadding.toPx() else size.width - startPadding.toPx()
drawLine(
color = lineColor,
start = Offset(x, verticalPadding.toPx()),
end = Offset(x, size.height - verticalPadding.toPx()),
strokeWidth = strokeWidth.toPx(),
)
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun AnimatedVisibilityScope.QuickActionItem(
state: QuickActionUM,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val hapticManager = LocalHapticManager.current
Row(
modifier = modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersMedium)
.clickable {
hapticManager.perform(TangemHapticEffect.View.SegmentTick)
onClick()
}
.padding(
vertical = TangemTheme.dimens.spacing2,
horizontal = TangemTheme.dimens.spacing12,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18),
) {
Box(
Modifier
.animateEnterExit(
enter = scaleIn(),
exit = scaleOut(),
)
.background(
color = TangemTheme.colors.button.secondary,
shape = CircleShape,
)
.size(TangemTheme.dimens.size32),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier
.requiredSize(TangemTheme.dimens.size16),
imageVector = ImageVector.vectorResource(id = state.icon),
contentDescription = null,
tint = TangemTheme.colors.button.primary,
)
}
Column(
modifier = Modifier
.animateEnterExit(
enter = fadeIn(),
exit = fadeOut(),
),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
) {
Text(
text = state.title.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = state.description.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
var isVisible by remember { mutableStateOf(true) }
Column(
modifier = Modifier
.fillMaxWidth()
.height(680.dp),
) {
Button(
onClick = { isVisible = !isVisible },
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
) {
Text(text = "Toggle")
}
SpacerH4()
Box(
modifier = Modifier.background(color = TangemTheme.colors.background.action),
) {
PortfolioQuickActions(
isVisible = isVisible,
onActionClick = {},
)
}
}
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewRtl() {
TangemThemePreview(rtl = true) {
Box(modifier = Modifier.background(color = TangemTheme.colors.background.action)) {
PortfolioQuickActions(
isVisible = true,
onActionClick = {},
)
}
}
}

View file

@ -0,0 +1,90 @@
package com.tangem.features.markets.portfolio.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
import com.tangem.core.ui.components.inputrow.InputRowChecked
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.rows.CornersToRound
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContent
import kotlinx.collections.immutable.toImmutableList
@Composable
fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<TokenActionsBSContent>(
config = config,
title = { content ->
TangemBottomSheetTitle(content.title)
},
containerColor = TangemTheme.colors.background.tertiary,
content = { Content(it) },
)
}
@Composable
private fun Content(content: TokenActionsBSContent) {
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
content.actions.forEachIndexed { index, action ->
val cornersToRound = when (index) {
0 -> CornersToRound.TOP_2
content.actions.lastIndex -> CornersToRound.BOTTOM_2
else -> CornersToRound.ZERO
}
DividerContainer(
modifier = Modifier
.clip(cornersToRound.getShape())
.background(TangemTheme.colors.background.action)
.clickable { content.onActionClick(action) },
showDivider = index != content.actions.lastIndex,
) {
InputRowChecked(
text = action.text,
checked = false,
)
}
}
}
}
@Preview(widthDp = 360, heightDp = 640)
@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview(
alwaysShowBottomSheets = true,
) {
Box(Modifier.background(TangemTheme.colors.background.secondary)) {
TokenActionsBottomSheet(
TangemBottomSheetConfig(
isShow = true,
onDismissRequest = {},
content = TokenActionsBSContent(
title = "Wallet 1",
actions = TokenActionsBSContent.Action.entries.toImmutableList(),
onActionClick = {},
),
),
)
}
}
}

View file

@ -0,0 +1,60 @@
package com.tangem.features.markets.portfolio.impl.ui.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM
import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM
import kotlinx.collections.immutable.persistentListOf
internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider<AddToPortfolioBSContentUM> {
override val values: Sequence<AddToPortfolioBSContentUM>
get() = sequenceOf(
AddToPortfolioBSContentUM(
selectedWallet = UserWalletItemUM(
id = UserWalletId("1"),
name = stringReference("Wallet 1"),
information = stringReference("3 cards, 10,123$"),
imageUrl = "",
isEnabled = true,
endIcon = UserWalletItemUM.EndIcon.Arrow,
onClick = {},
),
selectNetworkUM = SelectNetworkUM(
tokenId = "etherium",
tokenName = "Etherium",
tokenCurrencySymbol = "ETH",
networks = persistentListOf(
BlockchainRowUM(
name = "Etherium",
type = "MAIN",
iconResId = R.drawable.ic_eth_16,
isMainNetwork = true,
isSelected = true,
),
BlockchainRowUM(
name = "Etherium 2",
type = "TEST",
iconResId = R.drawable.ic_eth_16,
isMainNetwork = false,
isSelected = false,
),
BlockchainRowUM(
name = "Etherium 3",
type = "TEST",
iconResId = R.drawable.ic_eth_16,
isMainNetwork = false,
isSelected = false,
),
),
onNetworkSwitchClick = { _, _ -> },
iconUrl = null,
),
isScanCardNotificationVisible = true,
),
)
}

View file

@ -0,0 +1,50 @@
package com.tangem.features.markets.portfolio.impl.ui.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM
import kotlinx.collections.immutable.persistentListOf
internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider<MyPortfolioUM> {
override val values: Sequence<MyPortfolioUM>
get() = sequenceOf(
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken, sampleToken),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Available,
onAddClick = {},
),
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable,
onAddClick = {},
),
MyPortfolioUM.Tokens(
tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken),
buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading,
onAddClick = {},
),
MyPortfolioUM.AddFirstToken(
onAddClick = {},
),
MyPortfolioUM.Loading,
MyPortfolioUM.Unavailable,
)
val sampleToken = PortfolioTokenUM(
id = "",
networkId = "",
iconUrl = "",
balanceContent = PortfolioTokenUM.BalanceContent.TokenBalance(
balance = "486,65 \$",
tokenAmount = "733,71097 MATIC",
hidden = false,
),
title = "My wallet",
subtitle = "XRP Ledger token",
onClick = {},
onLongTap = {},
isQuickActionsShown = false,
onQuickActionClick = {},
)
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.markets.portfolio.impl.ui.state
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
internal data class AddToPortfolioBSContentUM(
val selectedWallet: UserWalletItemUM,
val selectNetworkUM: SelectNetworkUM,
val isScanCardNotificationVisible: Boolean,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,29 @@
package com.tangem.features.markets.portfolio.impl.ui.state
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed class MyPortfolioUM {
data class Tokens(
val tokens: ImmutableList<PortfolioTokenUM>,
val buttonState: AddButtonState,
val onAddClick: () -> Unit,
) : MyPortfolioUM() {
enum class AddButtonState {
Loading,
Available,
Unavailable,
}
}
data class AddFirstToken(
val onAddClick: () -> Unit,
) : MyPortfolioUM()
data object Loading : MyPortfolioUM()
data object Unavailable : MyPortfolioUM()
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.markets.portfolio.impl.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
internal data class PortfolioTokenUM(
val id: String,
val networkId: String,
val iconUrl: String,
val title: String,
val subtitle: String,
val balanceContent: BalanceContent,
val onClick: () -> Unit,
val onLongTap: () -> Unit,
val isQuickActionsShown: Boolean,
val onQuickActionClick: (QuickActionUM) -> Unit,
) {
// TODO add rest of the balance states ([REDACTED_TASK_KEY] [Markets] Portfolio token item UI Improvement)
@Immutable
sealed class BalanceContent {
data class TokenBalance( // TODO Add stacking ([REDACTED_TASK_KEY] [Markets] Add staking info to portfolio token item)
val balance: String,
val tokenAmount: String,
val hidden: Boolean,
) : BalanceContent()
data class Disabled(
val text: TextReference,
) : BalanceContent()
data object Loading : BalanceContent()
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.features.markets.portfolio.impl.ui.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.markets.impl.R
@Immutable
internal enum class QuickActionUM(
val title: TextReference,
val description: TextReference,
@DrawableRes val icon: Int,
) {
Buy(
title = resourceReference(R.string.common_buy),
description = resourceReference(R.string.buy_token_description),
icon = R.drawable.ic_plus_24,
),
Exchange(
title = resourceReference(R.string.common_exchange),
description = resourceReference(R.string.exсhange_token_description),
icon = R.drawable.ic_exchange_vertical_24,
),
Receive(
title = resourceReference(R.string.common_receive),
description = resourceReference(R.string.receive_token_description),
icon = R.drawable.ic_arrow_down_24,
),
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.markets.portfolio.impl.ui.state
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import kotlinx.collections.immutable.ImmutableList
internal data class SelectNetworkUM(
val tokenId: String,
val iconUrl: String?,
val tokenName: String,
val tokenCurrencySymbol: String,
val networks: ImmutableList<BlockchainRowUM>,
val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit,
)

View file

@ -0,0 +1,28 @@
package com.tangem.features.markets.portfolio.impl.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.ImmutableList
internal data class TokenActionsBSContent(
val title: String,
val actions: ImmutableList<Action>,
val onActionClick: (Action) -> Unit,
) : TangemBottomSheetConfigContent {
@Immutable
enum class Action(
val text: TextReference,
) {
CopyAddress(text = resourceReference(R.string.common_copy_address)),
Receive(text = resourceReference(R.string.common_receive)),
Sell(text = resourceReference(R.string.common_sell)),
Buy(text = resourceReference(R.string.common_buy)),
Send(text = resourceReference(R.string.common_send)),
Exchange(text = resourceReference(R.string.common_exchange)),
Stake(text = resourceReference(R.string.common_stake)),
}
}

View file

@ -25,16 +25,19 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetAllowanceUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
@ -54,8 +57,8 @@ import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@ -80,12 +83,17 @@ internal class StakingViewModel @Inject constructor(
private val submitHashUseCase: SubmitHashUseCase,
private val isStakeMoreAvailableUseCase: IsStakeMoreAvailableUseCase,
private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase,
private val updateDelayedNetworkStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
private val getAllowanceUseCase: GetAllowanceUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val isApproveNeededUseCase: IsApproveNeededUseCase,
private val clipboardManager: ClipboardManager,
private val vibratorHapticManager: VibratorHapticManager,
@DelayedWork private val coroutineScope: CoroutineScope,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents {
@ -575,7 +583,7 @@ internal class StakingViewModel @Inject constructor(
},
ifRight = { txHash ->
submitHash(transactionId, txHash)
updateStakeBalance()
scheduleUpdates()
val txUrl = getExplorerTransactionUrlUseCase(
txHash = txHash,
networkId = cryptoCurrencyStatus.currency.network.id,
@ -608,14 +616,55 @@ internal class StakingViewModel @Inject constructor(
}
}
private fun updateStakeBalance() {
viewModelScope.launch {
stakingYieldBalanceUseCase(
private fun scheduleUpdates() {
coroutineScope.launch {
listOf(
// we should update network to find pending tx after 1 sec
async {
fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrencyStatus.currency.network))
},
// we should update tx history and network for new balances
async {
updateStakeBalance()
},
async {
updateTxHistory()
},
async {
updateNetworkStatuses()
},
).awaitAll()
}
}
private suspend fun updateNetworkStatuses() {
updateDelayedNetworkStatusUseCase(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
delayMillis = BALANCE_UPDATE_DELAY,
refresh = true,
)
}
private suspend fun updateStakeBalance() {
stakingYieldBalanceUseCase(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
refresh = true,
)
}
private suspend fun updateTxHistory() {
delay(BALANCE_UPDATE_DELAY)
val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase(
userWalletId = userWalletId,
currency = cryptoCurrencyStatus.currency,
)
txHistoryItemsCountEither.onRight {
getTxHistoryItemsUseCase(
userWalletId = userWalletId,
address = CryptoCurrencyAddress(
cryptoCurrencyStatus.currency,
cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(),
),
currency = cryptoCurrencyStatus.currency,
refresh = true,
)
}
@ -630,5 +679,6 @@ internal class StakingViewModel @Inject constructor(
private companion object {
const val WHAT_IS_STAKING_ARTICLE_URL = "TODO staking"
const val ALLOWANCE_UPDATE_DELAY = 10_000L
const val BALANCE_UPDATE_DELAY = 11_000L
}
}

View file

@ -363,8 +363,8 @@ internal class StateBuilder(
if (quoteModel.permissionState is PermissionDataState.PermissionLoading) {
warnings.add(
SwapWarning.TransactionInProgressWarning(
title = resourceReference(R.string.warning_express_approval_in_progress_title),
description = resourceReference(R.string.warning_express_approval_in_progress_message),
title = stringReference("//TODO"),
description = stringReference("//TODO"),
),
)
} else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) {
@ -1009,8 +1009,8 @@ internal class StateBuilder(
warnings.add(
0,
SwapWarning.TransactionInProgressWarning(
title = resourceReference(R.string.warning_express_approval_in_progress_title),
description = resourceReference(R.string.warning_express_approval_in_progress_message),
title = stringReference("//TODO"),
description = stringReference("//TODO"),
),
)
return uiState.copy(

View file

@ -1,18 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.models.UserWallet
fun UserWallet.getCardsCount(): Int? {
return if (isMultiCurrency) {
when (val status = scanResponse.card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount + 1
is CardDTO.BackupStatus.NoBackup,
is CardDTO.BackupStatus.CardLinked,
-> 1
null -> 1 // Multi-currency wallet without backup function. Example, 4.12
}
} else {
null
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo

View file

@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.domain
import androidx.annotation.DrawableRes
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.impl.R

View file

@ -4,9 +4,9 @@ import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.visa.model.VisaCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount
import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBlockState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState

View file

@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState

View file

@ -1,9 +1,9 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import timber.log.Timber

View file

@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.utils.converter.Converter

View file

@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.utils.converter.Converter

View file

@ -1,10 +1,10 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels
import arrow.core.getOrElse
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount
import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState

View file

@ -223,19 +223,16 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
)
viewModelScope.launch(dispatchers.main) {
walletManagersFacade.getAddress(
walletManagersFacade.getDefaultAddress(
userWalletId = stateHolder.getSelectedWalletId(),
network = cryptoCurrencyStatus.currency.network,
)
.find { it.type == AddressType.Default }
?.value
?.let {
stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId()))
)?.let {
stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId()))
walletEventSender.send(
event = WalletEvent.CopyAddress(address = it),
)
}
walletEventSender.send(
event = WalletEvent.CopyAddress(address = it),
)
}
}
}

View file

@ -88,9 +88,9 @@ markdown = "0.7.2"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "release-app_5.14-742"
tangemBlockchainSdk = "develop-736"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "release-app_5.14-379"
tangemCardSdk = "develop-378"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
tangemVico = "2.0.0-alpha.25-tangem16"
#tangemVico = "0.0.1" # Keep it! - used for local builds ^