Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-03 12:36:09 +03:00
commit d617c9c30d
491 changed files with 15019 additions and 5050 deletions

View file

@ -708,6 +708,16 @@
"networkId": "cyber/test"
}
]
},
{
"id": "sei-network",
"name": "Sei Network",
"symbol": "SEI",
"networks": [
{
"networkId": "sei-network/test"
}
]
}
]
}

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

@ -0,0 +1,24 @@
package com.tangem.tap.common.analytics.handlers.firebase
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.tangem.core.analytics.AppInstanceIdProvider
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import kotlin.coroutines.resume
internal class FirebaseAppInstanceIdProvider : AppInstanceIdProvider {
override suspend fun getAppInstanceId(): String? = suspendCancellableCoroutine { continuation ->
Firebase.analytics.appInstanceId
.addOnSuccessListener { continuation.resume(it) }
.addOnFailureListener {
Timber.w("Fail to get appInstanceId")
continuation.resume(null)
}
}
override fun getAppInstanceIdSync(): String? {
return Firebase.analytics.appInstanceId.result
}
}

View file

@ -1,9 +1,11 @@
package com.tangem.tap.di.analytics
import com.tangem.core.analytics.AppInstanceIdProvider
import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase
import com.tangem.tap.common.analytics.DefaultAnalyticsContextProxy
import com.tangem.tap.common.analytics.DefaultChangeCardAnalyticsContextUseCase
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAppInstanceIdProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -23,4 +25,8 @@ internal object AnalyticsModule {
@Provides
@Singleton
fun provideAnalyticsContextProxy(): AnalyticsContextProxy = DefaultAnalyticsContextProxy()
@Provides
@Singleton
fun provideAppInstanceIdProvider(): AppInstanceIdProvider = FirebaseAppInstanceIdProvider()
}

View file

@ -92,4 +92,12 @@ internal object CardDomainModule {
fun provideNetworkHasDerivationUseCase(): NetworkHasDerivationUseCase {
return NetworkHasDerivationUseCase()
}
@Provides
@Singleton
fun provideIsRequiredDerivePublicKeysUseCase(
derivationsRepository: DerivationsRepository,
): HasMissedDerivationsUseCase {
return HasMissedDerivationsUseCase(derivationsRepository)
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.tap.di.domain
import com.tangem.domain.managetokens.GetManagedTokensUseCase
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.repository.CustomTokensRepository
import com.tangem.domain.managetokens.repository.ManageTokensRepository
import dagger.Module
import dagger.Provides
@ -17,4 +19,66 @@ internal object ManageTokensDomainModule {
fun provideGetManageTokensUseCase(manageTokensRepository: ManageTokensRepository): GetManagedTokensUseCase {
return GetManagedTokensUseCase(manageTokensRepository)
}
@Provides
@Singleton
fun provideValidateTokenFormatUseCase(customTokensRepository: CustomTokensRepository): ValidateTokenFormUseCase {
return ValidateTokenFormUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideCreateCurrencyUseCase(customTokensRepository: CustomTokensRepository): CreateCurrencyUseCase {
return CreateCurrencyUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideFindTokenUseCase(customTokensRepository: CustomTokensRepository): FindTokenUseCase {
return FindTokenUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideCheckIsCurrencyNotAddedUseCase(
customTokensRepository: CustomTokensRepository,
): CheckIsCurrencyNotAddedUseCase {
return CheckIsCurrencyNotAddedUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideRemoveCustomManagedCryptoCurrencyUseCase(
customTokensRepository: CustomTokensRepository,
): RemoveCustomManagedCryptoCurrencyUseCase {
return RemoveCustomManagedCryptoCurrencyUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideSaveManagedTokensUseCase(
manageTokensRepository: ManageTokensRepository,
derivationsRepository: DerivationsRepository,
): SaveManagedTokensUseCase {
return SaveManagedTokensUseCase(
manageTokensRepository = manageTokensRepository,
derivationsRepository = derivationsRepository,
)
}
@Provides
@Singleton
fun provideGetSupportedNetworksUseCase(
customTokensRepository: CustomTokensRepository,
): GetSupportedNetworksUseCase {
return GetSupportedNetworksUseCase(customTokensRepository)
}
@Provides
@Singleton
fun provideValidateDerivationPathUseCase(
customTokensRepository: CustomTokensRepository,
): ValidateDerivationPathUseCase {
return ValidateDerivationPathUseCase(customTokensRepository)
}
}

View file

@ -1,10 +1,8 @@
package com.tangem.tap.di.domain
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
import com.tangem.domain.markets.GetTokenPriceChartUseCase
import com.tangem.domain.markets.GetTokenQuotesUseCase
import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -37,7 +35,13 @@ object MarketsDomainModule {
@Provides
@Singleton
fun provideGetTokenQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenQuotesUseCase {
return GetTokenQuotesUseCase(marketsTokenRepository = marketsTokenRepository)
fun provideTokenFullQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenFullQuotesUseCase {
return GetTokenFullQuotesUseCase(marketsTokenRepository = marketsTokenRepository)
}
@Provides
@Singleton
fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetCurrencyQuotesUseCase {
return GetCurrencyQuotesUseCase(quotesRepository = quotesRepository)
}
}

View file

@ -145,18 +145,6 @@ internal object StakingDomainModule {
)
}
@Provides
@Singleton
fun provideIsStakeMoreAvailableUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): IsStakeMoreAvailableUseCase {
return IsStakeMoreAvailableUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideIsApproveNeededUseCase(

View file

@ -104,6 +104,22 @@ internal object TokensDomainModule {
)
}
@Provides
@Singleton
fun provideGetAllWalletsCryptoCurrencyStatusesUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): GetAllWalletsCryptoCurrencyStatusesUseCase {
return GetAllWalletsCryptoCurrencyStatusesUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
}
@Provides
@Singleton
fun provideGetCurrencyWarningsUseCase(
@ -158,8 +174,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
@ -419,4 +436,10 @@ internal object TokensDomainModule {
fun provideCheckHasLinkedTokensUseCase(currenciesRepository: CurrenciesRepository): CheckHasLinkedTokensUseCase {
return CheckHasLinkedTokensUseCase(currenciesRepository)
}
@Provides
@Singleton
fun provideGetCurrencyCheckUseCase(currencyChecksRepository: CurrencyChecksRepository): GetCurrencyCheckUseCase {
return GetCurrencyCheckUseCase(currencyChecksRepository)
}
}

View file

@ -42,6 +42,21 @@ internal object TransactionDomainModule {
)
}
@Provides
@Singleton
fun provideSendMultipleTransactionUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
transactionRepository: TransactionRepository,
walletManagersFacade: WalletManagersFacade,
): SendMultipleTransactionUseCase {
return SendMultipleTransactionUseCase(
demoConfig = DemoConfig(),
cardSdkConfigRepository = cardSdkConfigRepository,
transactionRepository = transactionRepository,
walletManagersFacade = walletManagersFacade,
)
}
@Provides
@Singleton
fun provideAssociateAssetUseCase(

View file

@ -1,5 +1,7 @@
package com.tangem.tap.domain.card
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.TangemSdkError
@ -8,10 +10,13 @@ import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.common.currency.getNetwork
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
@ -30,6 +35,10 @@ internal class DefaultDerivationsRepository(
) : DerivationsRepository {
override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network))
}
override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List<Network>) {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) {
@ -38,7 +47,7 @@ internal class DefaultDerivationsRepository(
}
val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse)
.find(currencies)
.findByNetworks(networks)
.ifEmpty {
Timber.d("Nothing to derive")
return
@ -47,6 +56,26 @@ internal class DefaultDerivationsRepository(
derivePublicKeys(userWalletId = userWalletId, derivations = derivations)
}
override suspend fun hasMissedDerivations(
userWalletId: UserWalletId,
networksWithDerivationPath: Map<Network.ID, String?>,
): Boolean {
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse)
.findByNetworks(
networksWithDerivationPath.mapNotNull { (networkId, extraDerivationPath) ->
getNetwork(
blockchain = Blockchain.fromNetworkId(networkId.value) ?: return@mapNotNull null,
extraDerivationPath = extraDerivationPath,
derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider,
)
},
)
return derivations.isNotEmpty()
}
override suspend fun derivePublicKeys(userWalletId: UserWalletId, derivations: Derivations): DerivedKeys {
tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)
.doOnSuccess { response ->

View file

@ -11,6 +11,7 @@ import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.KeyWalletPublicKey
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.operations.derivation.ExtendedPublicKeysMap
private typealias DerivationData = Pair<ByteArrayKey, List<DerivationPath>>
@ -26,8 +27,12 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
/** Find missed derivations for given currencies [currencies] */
fun find(currencies: List<CryptoCurrency>): Derivations {
return currencies.map { it.network }.let(::findByNetworks)
}
fun findByNetworks(networks: List<Network>): Derivations {
return buildMap<ByteArrayKey, MutableList<DerivationPath>> {
currencies
networks
.mapToNewDerivations()
.forEach { data ->
val current = this[data.first]
@ -41,25 +46,25 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
}
}
private fun List<CryptoCurrency>.mapToNewDerivations(): List<DerivationData> {
private fun List<Network>.mapToNewDerivations(): List<DerivationData> {
val config = CardConfig.createConfig(scanResponse.card)
return mapNotNull { currency ->
val blockchain = Blockchain.fromId(id = currency.network.id.value)
return mapNotNull { network ->
val blockchain = Blockchain.fromId(id = network.id.value)
val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null
findNewDerivations(curve = curve, scanResponse = scanResponse, currency = currency)
findNewDerivations(curve = curve, scanResponse = scanResponse, network = network)
}
}
private fun findNewDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: CryptoCurrency,
network: Network,
): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val publicKey = wallet.publicKey.toMapKey()
val derivationCandidates = currency
val derivationCandidates = network
.getDerivationCandidates(curve)
.ifEmpty { return null }
.filterAlreadyDerivedKeys(publicKey)
@ -68,13 +73,13 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
return publicKey to derivationCandidates
}
private fun CryptoCurrency.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
val blockchain = Blockchain.fromId(id = network.id.value)
private fun Network.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
val blockchain = Blockchain.fromId(id = this.id.value)
return buildList {
add(blockchain.getDerivationPath(curve = curve))
add(blockchain.getCustomDerivationPath(curve = curve, currency = this@getDerivationCandidates))
add(blockchain.getCardanoDerivationPathIfNeeded(currency = this@getDerivationCandidates))
add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates))
add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates))
}
.filterNotNull()
.distinct()
@ -88,17 +93,17 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
}
}
private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, currency: CryptoCurrency): DerivationPath? {
private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? {
return if (getSupportedCurves().contains(curve)) {
currency.network.derivationPath.value?.let(::DerivationPath)
network.derivationPath.value?.let(::DerivationPath)
} else {
null
}
}
private fun Blockchain.getCardanoDerivationPathIfNeeded(currency: CryptoCurrency): DerivationPath? {
return if (currency is CryptoCurrency.Coin && this == Blockchain.Cardano) {
currency.network.derivationPath.value?.let {
private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? {
return if (this == Blockchain.Cardano) {
network.derivationPath.value?.let {
CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it))
}
} else {

View file

@ -111,6 +111,10 @@ object WalletMockContent : MockContent {
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
DerivationPath("m/44'/195'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
),
extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
@ -189,6 +193,13 @@ object WalletMockContent : MockContent {
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/195'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
),

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

@ -7,7 +7,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
@ -20,12 +20,12 @@ internal fun SettingsAlertDialog(dialog: Dialog.Alert) {
title = dialog.title.resolveReference(),
message = dialog.description.resolveReference(),
isDismissable = false,
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = dialog.confirmText.resolveReference(),
warning = true,
onClick = dialog.onConfirm,
),
dismissButton = DialogButton(
dismissButton = DialogButtonUM(
title = stringResource(id = R.string.common_cancel),
onClick = dialog.onDismiss,
),

View file

@ -6,7 +6,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.SelectorDialog
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
@ -21,7 +21,7 @@ internal fun SettingsSelectorDialog(dialog: Dialog.Selector) {
title = dialog.title.resolveReference(),
selectedItemIndex = dialog.selectedItemIndex,
items = dialog.items.map { it.resolveReference() }.toImmutableList(),
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(R.string.common_cancel),
onClick = dialog.onDismiss,
),

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

@ -190,11 +190,11 @@ private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) {
BasicDialog(
title = stringResource(dialog.titleResId),
message = stringResource(dialog.messageResId),
dismissButton = DialogButton(
dismissButton = DialogButtonUM(
title = stringResource(id = R.string.common_cancel),
onClick = dialog.onDismiss,
),
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.card_settings_action_sheet_reset),
warning = true,
onClick = dialog.onConfirmClick,
@ -208,7 +208,7 @@ private fun CompletedResetDialog(dialog: ResetCardDialog) {
BasicDialog(
title = stringResource(id = dialog.titleResId),
message = stringResource(id = dialog.messageResId),
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
onClick = dialog.onConfirmClick,
),

View file

@ -98,7 +98,7 @@ internal class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
viewModel.onSearchClick()
} else {
Analytics.send(IntroductionProcess.ButtonTokensList())
store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) }
store.dispatchNavigationAction { push(AppRoute.ManageTokens()) }
store.dispatch(TokensAction.SetArgs.ReadAccess)
}
},

View file

@ -61,7 +61,7 @@ internal class HomeViewModel @Inject constructor(
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList())
store.dispatch(TokensAction.SetArgs.ReadAccess)
store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) }
store.dispatchNavigationAction { push(AppRoute.ManageTokens()) }
}
private fun scanCard() {

View file

@ -9,9 +9,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
@Composable
@ -19,11 +19,11 @@ fun EnrollBiometricsDialogContent(dialog: EnrollBiometricsDialog) {
BasicDialog(
title = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_title),
message = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_description),
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(R.string.common_enable),
onClick = dialog.onEnroll,
),
dismissButton = DialogButton(
dismissButton = DialogButtonUM(
onClick = dialog.onCancel,
),
onDismissDialog = dialog.onCancel,

View file

@ -5,7 +5,6 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkAddress
@ -48,12 +47,6 @@ object TradeCryptoMiddleware {
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
is TradeCryptoAction.Buy -> proceedBuyAction(state, action)
is TradeCryptoAction.Sell -> proceedSellAction(action)
is TradeCryptoAction.Swap -> openSwap(currency = action.cryptoCurrency)
is TradeCryptoAction.Stake -> openStaking(
userWalletId = action.userWalletId,
cryptoCurrencyId = action.cryptoCurrencyId,
yield = action.yield,
)
is TradeCryptoAction.SendToken -> handleNewSendToken(action = action)
is TradeCryptoAction.SendCoin -> handleNewSendCoin(action = action)
}
@ -141,22 +134,6 @@ object TradeCryptoMiddleware {
)?.let { store.dispatchOpenUrl(it) }
}
private fun openSwap(currency: CryptoCurrency) {
store.dispatchNavigationAction { push(AppRoute.Swap(currency = currency)) }
}
private fun openStaking(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, yield: Yield) {
store.dispatchNavigationAction {
push(
AppRoute.Staking(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrencyId,
yield = yield,
),
)
}
}
private fun handleNewSendToken(action: TradeCryptoAction.SendToken) {
handleNewSend(
userWalletId = action.userWallet.walletId,

View file

@ -7,7 +7,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.welcome.ui.model.WarningModel
import com.tangem.wallet.R
@ -27,7 +27,7 @@ internal fun WarningDialog(warning: WarningModel?) {
},
),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),
@ -38,7 +38,7 @@ internal fun WarningDialog(warning: WarningModel?) {
title = stringResource(id = R.string.common_attention),
message = stringResource(id = R.string.key_invalidated_warning_description),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),
@ -50,7 +50,7 @@ internal fun WarningDialog(warning: WarningModel?) {
message = stringResource(id = R.string.biometric_unavailable_warning),
onDismissDialog = warning.onDismiss,
isDismissable = false,
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),

View file

@ -11,6 +11,7 @@ import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.managetokens.ManageTokensToggles
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.send.api.navigation.SendRouter
@ -50,6 +51,7 @@ internal class ChildFactory @Inject constructor(
private val walletSettingsComponentFactory: WalletSettingsComponent.Factory,
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
private val sendRouter: SendRouter,
private val tokenDetailsRouter: TokenDetailsRouter,
private val walletRouter: WalletRouter,
@ -126,13 +128,7 @@ internal class ChildFactory @Inject constructor(
if (manageTokensToggles.isFeatureEnabled) {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = ManageTokensComponent.Params(
mode = if (route.readOnlyContent) {
ManageTokensComponent.Mode.READ_ONLY
} else {
ManageTokensComponent.Mode.MANAGE
},
),
params = ManageTokensComponent.Params(route.userWalletId),
componentFactory = manageTokensComponentFactory,
)
} else {
@ -191,6 +187,16 @@ internal class ChildFactory @Inject constructor(
componentFactory = walletSettingsComponentFactory,
)
}
is AppRoute.MarketsTokenDetails -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = MarketsTokenDetailsComponent.Params(
token = route.token,
appCurrency = route.appCurrency,
),
componentFactory = marketsTokenDetailsComponentFactory,
)
}
}
}

View file

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/llTotalContainer"
android:layout_width="match_parent"
android:layout_height="64dp"
android:gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:id="@+id/llTotal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/send_total_label"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvTotalValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textAllCaps="true"
android:textSize="20sp"
android:textStyle="bold"
tools:text="usd" />
</FrameLayout>
<TextView
android:id="@+id/tvWillBeSentValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="4dp"
android:gravity="end"
android:textColor="@color/text_tertiary"
tools:text="123.29837729 ADA 39487593.109342039402938049 will be sent" />
</LinearLayout>
<FrameLayout
android:id="@+id/flTotalTokenCrypto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<TextView
android:id="@+id/tvTotalTokenCrypto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/send_total_label"
android:textColor="@color/text_tertiary"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvTotalTokenCryptoValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textAllCaps="true"
android:textColor="@color/text_tertiary"
android:textSize="14sp"
android:textStyle="bold"
tools:text="usd" />
</FrameLayout>
</LinearLayout>

View file

@ -1,8 +1,12 @@
package com.tangem.tap.domain.card
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.FeePaidCurrency
import com.tangem.blockchain.common.Token
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.getNetworkDerivationPath
import com.tangem.data.common.currency.getNetworkStandardType
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
@ -40,11 +44,23 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) {
}
private fun createCoin(blockchain: Blockchain): CryptoCurrency {
return factory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = scanResponse.derivationStyleProvider,
)!!
val network = Network(
id = Network.ID(blockchain.id),
backendId = blockchain.toNetworkId(),
name = blockchain.getNetworkName(),
isTestnet = blockchain.isTestnet(),
derivationPath = getNetworkDerivationPath(
blockchain,
extraDerivationPath = null,
scanResponse.derivationStyleProvider,
),
currencySymbol = blockchain.currency,
standardType = getNetworkStandardType(blockchain),
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource,
canHandleTokens = false,
)
return factory.createCoin(network = network)
}
// Impossible to create custom token by CryptoCurrencyFactory because it works with URI under the hood
@ -68,6 +84,7 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) {
isTestnet = false,
standardType = Network.StandardType.ERC20,
hasFiatFeeRate = true,
canHandleTokens = true,
),
name = "NEVER-MIND",
symbol = "NEVER-MIND",

View file

@ -19,6 +19,8 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.markets.models)
implementation(projects.domain.appCurrency.models)
/* Libs - Other */
api(deps.kotlin.serialization)

View file

@ -5,6 +5,8 @@ import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrency
@ -177,8 +179,8 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class ManageTokens(
val readOnlyContent: Boolean,
) : AppRoute(path = "/manage_tokens/$readOnlyContent"), RouteBundleParams {
val userWalletId: UserWalletId? = null,
) : AppRoute(path = "/manage_tokens/$userWalletId"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}
@ -217,12 +219,14 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class Swap(
val currency: CryptoCurrency,
) : AppRoute(path = "/swap/${currency.id.value}"), RouteBundleParams {
val userWalletId: UserWalletId,
) : AppRoute(path = "/swap/${currency.id.value}/${userWalletId.stringValue}"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val CURRENCY_BUNDLE_KEY = "currency"
const val USER_WALLET_ID_KEY = "userWalletId"
}
}
@ -262,4 +266,10 @@ sealed class AppRoute(val path: String) : Route {
data class WalletSettings(
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_settings/${userWalletId.stringValue}")
@Serializable
data class MarketsTokenDetails(
val token: TokenMarketParams,
val appCurrency: AppCurrency,
) : AppRoute(path = "/markets_token_details/${token.id}")
}

View file

@ -0,0 +1,51 @@
package com.tangem.common.routing.utils
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.decompose.navigation.Router
import kotlin.reflect.KClass
/**
* Temporary solution to convert [AppRouter] to [Router].
* (through manual ComponentContext creation).
*
* **Will be removed when all screens will be migrated to Decompose.**
*
* @return [Router] that wraps [AppRouter].
*/
fun AppRouter.asRouter(): Router {
return RouterProxy(appRouter = this)
}
private class RouterProxy(
private val appRouter: AppRouter,
) : Router {
override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
if (route is AppRoute) {
appRouter.push(route, onComplete)
}
}
override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) {
routes.filterIsInstance<AppRoute>().let {
appRouter.replaceAll(*it.toTypedArray(), onComplete = onComplete)
}
}
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
appRouter.pop(onComplete)
}
override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
if (route is AppRoute) {
appRouter.popTo(route, onComplete)
}
}
@Suppress("UNCHECKED_CAST")
override fun popTo(routeClass: KClass<out Route>, onComplete: (isSuccess: Boolean) -> Unit) {
appRouter.popTo(routeClass as KClass<out AppRoute>, onComplete)
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.common.ui.charts.state
import kotlinx.collections.immutable.toImmutableList
fun MarketChartData.Data.sorted(): MarketChartData.Data {
val points = this.x.zip(this.y).sortedBy { it.first }
val (x, y) = points.unzip()
return MarketChartData.Data(
x = x.toImmutableList(),
y = y.toImmutableList(),
)
}

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)
@ -27,9 +28,14 @@ dependencies {
implementation(projects.core.utils)
/** Project - Domain */
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.legacy)
implementation(projects.domain.staking.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.transaction.models)
implementation(projects.domain.wallets.models)
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}

View file

@ -0,0 +1,49 @@
package com.tangem.common.ui.alerts
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
import com.tangem.common.ui.alerts.models.AlertTransactionErrorUM
import com.tangem.common.ui.alerts.models.AlertUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.utils.converter.Converter
class SendTransactionAlertConverter(
private val popBackStack: () -> Unit,
private val onFailedTxEmailClick: (String) -> Unit,
) : Converter<SendTransactionError, AlertUM?> {
override fun convert(value: SendTransactionError): AlertUM? {
return when (value) {
is SendTransactionError.DemoCardError -> AlertDemoModeUM(
onConfirmClick = popBackStack,
)
is SendTransactionError.TangemSdkError -> AlertTransactionErrorUM(
code = value.code.toString(),
cause = null,
causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)),
onConfirmClick = { onFailedTxEmailClick(value.code.toString()) },
)
is SendTransactionError.BlockchainSdkError -> AlertTransactionErrorUM(
code = value.code.toString(),
cause = value.message,
onConfirmClick = { onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") },
)
is SendTransactionError.DataError -> AlertTransactionErrorUM(
code = "",
cause = value.message,
onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) },
)
is SendTransactionError.NetworkError -> AlertTransactionErrorUM(
code = value.code.orEmpty(),
cause = value.message.orEmpty(),
onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) },
)
is SendTransactionError.UnknownError -> AlertTransactionErrorUM(
code = "",
cause = value.ex?.localizedMessage,
onConfirmClick = { onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) },
)
else -> null
}
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.common.ui.alerts.models
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
data class AlertDemoModeUM(
override val onConfirmClick: () -> Unit,
) : AlertUM {
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title)
override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message)
}

View file

@ -0,0 +1,21 @@
package com.tangem.common.ui.alerts.models
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
data class AlertTransactionErrorUM(
val code: String,
val cause: String?,
val causeTextReference: TextReference? = null,
override val onConfirmClick: (() -> Unit)? = null,
) : AlertUM {
override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title)
override val message: TextReference = resourceReference(
id = R.string.send_alert_transaction_failed_text,
formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code),
)
override val confirmButtonText: TextReference =
resourceReference(id = R.string.common_support)
}

View file

@ -0,0 +1,12 @@
package com.tangem.common.ui.alerts.models
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
interface AlertUM {
val title: TextReference?
val message: TextReference
val confirmButtonText: TextReference
val onConfirmClick: (() -> Unit)?
}

View file

@ -1,6 +1,7 @@
package com.tangem.common.ui.amountScreen.converters.field
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountState
@ -81,6 +82,10 @@ class AmountFieldChangeTransformer(
cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO),
fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO),
isError = false,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.None,
keyboardType = KeyboardType.Number,
),
),
)
}

View file

@ -59,7 +59,7 @@ fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) {
BasicDialog(
message = content.data.dialogText.resolveReference(),
title = stringResource(id = R.string.common_approve),
confirmButton = DialogButton { isPermissionAlertShow = false },
confirmButton = DialogButtonUM { isPermissionAlertShow = false },
onDismissDialog = {},
)
}

View file

@ -7,8 +7,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -44,25 +43,15 @@ fun NavigationButtonsBlock(buttonState: NavigationButtonsState, modifier: Modifi
PreviousButton(state?.prevButton)
PrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f))
}
SecondaryButton(state?.secondaryButton)
}
}
@Composable
private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) {
val wrappedButton by rememberNavigationButton(primaryButton)
AnimatedContent(
targetState = primaryButton,
transitionSpec = {
val isPrimaryToHide = targetState != null && initialState == null
val isPrimaryWasVisible = targetState == null && initialState != null
if (isPrimaryToHide || isPrimaryWasVisible) {
slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn())
.togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()))
} else {
fadeIn().togetherWith(fadeOut())
}
},
targetState = wrappedButton,
transitionSpec = { navigationButtonsTransition() },
contentAlignment = Alignment.Center,
label = "Animate show primary button",
modifier = modifier.fillMaxWidth(),
@ -88,43 +77,6 @@ private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier =
}
}
@Composable
private fun SecondaryButton(secondaryButton: NavigationButton?) {
AnimatedContent(
targetState = secondaryButton,
transitionSpec = {
val isPrimaryToHide = targetState != null && initialState == null
val isPrimaryWasVisible = targetState == null && initialState != null
if (isPrimaryToHide || isPrimaryWasVisible) {
slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn())
.togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()))
} else {
fadeIn().togetherWith(fadeOut())
}
},
contentAlignment = Alignment.Center,
label = "Animate show secondary button",
modifier = Modifier.fillMaxWidth(),
) { button ->
if (button != null && button.textReference != TextReference.EMPTY) {
val icon = button.iconRes?.let { TangemButtonIconPosition.End(iconResId = it) }
?: TangemButtonIconPosition.None
TangemButton(
text = button.textReference.resolveReference(),
enabled = button.isEnabled,
onClick = button.onClick,
icon = icon,
showProgress = button.showProgress,
colors = TangemButtonsDefaults.secondaryButtonColors,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
)
} else {
Spacer(modifier = Modifier.fillMaxWidth())
}
}
}
@Composable
private fun PreviousButton(prevButton: NavigationButton?) {
AnimatedVisibility(
@ -182,6 +134,28 @@ private fun ExtraButtons(extraButtons: ImmutableList<NavigationButton>?, txUrl:
}
}
@Composable
private fun rememberNavigationButton(button: NavigationButton?): MutableState<NavigationButton?> {
return remember(
button?.iconRes,
button?.isIconVisible,
button?.isEnabled,
button?.showProgress,
button?.textReference,
) { mutableStateOf(button) }
}
private fun <T> AnimatedContentTransitionScope<T>.navigationButtonsTransition(): ContentTransform {
val isPrimaryToHide = targetState != null && initialState == null
val isPrimaryWasVisible = targetState == null && initialState != null
return if (isPrimaryToHide || isPrimaryWasVisible) {
slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn())
.togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()))
} else {
fadeIn().togetherWith(fadeOut())
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -10,7 +10,6 @@ sealed class NavigationButtonsState {
data class Data(
val primaryButton: NavigationButton,
val prevButton: NavigationButton?,
val secondaryButton: NavigationButton?,
val extraButtons: ImmutableList<NavigationButton>,
val txUrl: String? = null,
) : NavigationButtonsState()

View file

@ -30,14 +30,6 @@ internal object NavigationButtonsPreview {
),
)
private val next = NavigationButton(
textReference = resourceReference(R.string.common_next),
isSecondary = false,
isIconVisible = false,
showProgress = false,
isEnabled = true,
onClick = {},
)
private val prev = NavigationButton(
textReference = TextReference.EMPTY,
iconRes = R.drawable.ic_back_24,
@ -60,7 +52,6 @@ internal object NavigationButtonsPreview {
val allButtons = NavigationButtonsState.Data(
primaryButton = finished,
prevButton = prev,
secondaryButton = next,
extraButtons = extraButtons,
txUrl = "https://tangem.com",
)

View file

@ -0,0 +1,251 @@
package com.tangem.common.ui.notifications
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.ui.R
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import java.math.BigDecimal
sealed class NotificationUM(val config: NotificationConfig) {
open class Error(
title: TextReference,
subtitle: TextReference,
iconResId: Int = R.drawable.ic_alert_24,
buttonState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
) : NotificationUM(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = iconResId,
buttonsState = buttonState,
onCloseClick = onCloseClick,
),
) {
data object TotalExceedsBalance : Error(
title = resourceReference(R.string.send_notification_exceed_balance_title),
subtitle = resourceReference(R.string.send_notification_exceed_balance_text),
)
data object InvalidAmount : Error(
title = resourceReference(R.string.send_notification_invalid_amount_title),
subtitle = resourceReference(R.string.send_notification_invalid_amount_text),
)
data class MinimumAmountError(val amount: String) : Error(
title = resourceReference(R.string.send_notification_invalid_amount_title),
subtitle = resourceReference(
R.string.send_notification_invalid_minimum_amount_text,
wrappedList(amount, amount),
),
)
data class TransactionLimitError(
val cryptoCurrency: String,
val utxoLimit: String,
val amountLimit: String,
val onConfirmClick: () -> Unit,
) : Error(
title = resourceReference(R.string.send_notification_transaction_limit_title),
subtitle = resourceReference(
R.string.send_notification_transaction_limit_text,
wrappedList(cryptoCurrency, utxoLimit, amountLimit),
),
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_leave_button, wrappedList(amountLimit)),
onClick = onConfirmClick,
),
)
data class ExceedsBalance(
val networkIconId: Int,
val currencyName: String,
val feeName: String,
val feeSymbol: String,
val networkName: String,
val mergeFeeNetworkName: Boolean = false,
val onClick: (() -> Unit)? = null,
) : Error(
title = resourceReference(
id = R.string.warning_send_blocked_funds_for_fee_title,
wrappedList(feeName),
),
subtitle = resourceReference(
id = R.string.warning_send_blocked_funds_for_fee_message,
formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol),
),
iconResId = networkIconId,
buttonState = onClick?.let {
NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(
R.string.common_buy_currency,
wrappedList(
if (mergeFeeNetworkName) {
"$currencyName ($feeSymbol)"
} else {
feeName
},
),
),
onClick = onClick,
)
},
)
data class ExistentialDeposit(val deposit: String, val onConfirmClick: () -> Unit) : Error(
title = resourceReference(R.string.send_notification_existential_deposit_title),
subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)),
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_leave_button, wrappedList(deposit)),
onClick = onConfirmClick,
),
)
data class ReserveAmount(val amount: String) : Error(
title = resourceReference(
id = R.string.send_notification_invalid_reserve_amount_title,
wrappedList(amount),
),
subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text),
)
}
open class Warning(
title: TextReference,
subtitle: TextReference,
iconResId: Int = R.drawable.img_attention_20,
buttonsState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
) : NotificationUM(
config = NotificationConfig(
title = title,
subtitle = subtitle,
iconResId = iconResId,
buttonsState = buttonsState,
onCloseClick = onCloseClick,
),
) {
data class HighFeeError(
val currencyName: String,
val amount: String,
val onConfirmClick: () -> Unit,
val onCloseClick: () -> Unit,
) : Warning(
title = resourceReference(R.string.send_notification_high_fee_title),
subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)),
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)),
onClick = onConfirmClick,
),
onCloseClick = onCloseClick,
)
data object FeeTooLow : Warning(
title = resourceReference(id = R.string.send_notification_transaction_delay_title),
subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text),
)
data class TooHigh(
val value: String,
) : Warning(
title = resourceReference(id = R.string.send_notification_fee_too_high_title),
subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)),
)
data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning(
title = resourceReference(R.string.send_fee_unreachable_error_title),
subtitle = resourceReference(R.string.send_fee_unreachable_error_text),
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.warning_button_refresh),
onClick = onRefresh,
),
)
data class TronAccountNotActivated(val tokenName: String) : Warning(
title = resourceReference(R.string.send_fee_unreachable_error_title),
subtitle = resourceReference(
R.string.send_tron_account_activation_error,
wrappedList(tokenName),
),
)
data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning(
title = resourceReference(R.string.send_network_fee_warning_title),
subtitle = resourceReference(
R.string.common_network_fee_warning_content,
wrappedList(cryptoAmount, fiatAmount),
),
)
}
sealed interface Cardano {
data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Warning(
title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title),
subtitle = resourceReference(
id = R.string.cardano_coin_will_be_send_with_token_description,
formatArgs = wrappedList(minAdaValue, tokenName),
),
)
data object InsufficientBalanceToTransferCoin : Error(
title = resourceReference(id = R.string.cardano_max_amount_has_token_title),
subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description),
)
data class InsufficientBalanceToTransferToken(val tokenName: String) : Error(
title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title),
subtitle = resourceReference(
id = R.string.cardano_insufficient_balance_to_send_token_description,
formatArgs = wrappedList(tokenName),
),
)
}
sealed interface Koinos {
data class InsufficientRecoverableMana(
val mana: BigDecimal,
val maxMana: BigDecimal,
) : Error(
title = resourceReference(R.string.koinos_insufficient_mana_to_send_koin_title),
subtitle = resourceReference(
R.string.koinos_insufficient_mana_to_send_koin_description,
formatArgs = wrappedList(
BigDecimalFormatter.formatCryptoAmountShorted(mana, "", Blockchain.Koinos.decimals()),
BigDecimalFormatter.formatCryptoAmountShorted(maxMana, "", Blockchain.Koinos.decimals()),
),
),
)
data object InsufficientBalance : Error(
title = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_title),
subtitle = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_description),
)
data class ManaExceedsBalance(
val availableKoinForTransfer: BigDecimal,
val onReduceClick: () -> Unit,
) : Error(
title = resourceReference(R.string.koinos_mana_exceeds_koin_balance_title),
subtitle = resourceReference(
R.string.koinos_mana_exceeds_koin_balance_description,
formatArgs = wrappedList(
BigDecimalFormatter.formatCryptoAmount(
availableKoinForTransfer,
Blockchain.Koinos.currency,
Blockchain.Koinos.decimals(),
),
),
),
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.send_notification_reduce_to, wrappedList(availableKoinForTransfer)),
onClick = onReduceClick,
),
)
}
}

View file

@ -0,0 +1,362 @@
package com.tangem.common.ui.notifications
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.amountScreen.utils.getFiatString
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.transaction.error.GetFeeError
import java.math.BigDecimal
@Suppress("LargeClass")
object NotificationsFactory {
fun MutableList<NotificationUM>.addFeeUnreachableNotification(
feeError: GetFeeError,
tokenName: String,
onReload: () -> Unit,
) {
when (feeError) {
is GetFeeError.BlockchainErrors.TronActivationError -> add(
NotificationUM.Warning.TronAccountNotActivated(tokenName),
)
is GetFeeError.DataError,
is GetFeeError.UnknownError,
-> add(
NotificationUM.Warning.NetworkFeeUnreachable(onReload),
)
else -> {
/* do nothing */
}
}
}
fun MutableList<NotificationUM>.addExceedBalanceNotification(
feeAmount: BigDecimal,
sendingAmount: BigDecimal,
isSubtractionAvailable: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus,
) {
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
if (!isSubtractionAvailable) return
val showNotification = sendingAmount + feeAmount > balance
if (showNotification) {
add(NotificationUM.Error.TotalExceedsBalance)
}
}
fun MutableList<NotificationUM>.addReserveAmountErrorNotification(
reserveAmount: BigDecimal?,
sendingAmount: BigDecimal,
cryptoCurrency: CryptoCurrency,
isAccountFunded: Boolean,
) {
if (!isAccountFunded && reserveAmount != null && reserveAmount > sendingAmount) {
add(
NotificationUM.Error.ReserveAmount(
BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = sendingAmount,
cryptoCurrency = cryptoCurrency,
),
),
)
}
}
fun MutableList<NotificationUM>.addTransactionLimitErrorNotification(
utxoLimit: UtxoAmountLimit?,
cryptoCurrency: CryptoCurrency,
onReduceClick: (
reduceAmountTo: BigDecimal,
notification: Class<out NotificationUM>,
) -> Unit,
) {
if (utxoLimit != null) {
add(
NotificationUM.Error.TransactionLimitError(
cryptoCurrency = cryptoCurrency.name,
utxoLimit = utxoLimit.maxLimit.toPlainString(),
amountLimit = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = utxoLimit.maxAmount,
cryptoCurrency = cryptoCurrency,
),
onConfirmClick = {
onReduceClick(
utxoLimit.maxAmount,
NotificationUM.Error.TransactionLimitError::class.java,
)
},
),
)
}
}
fun MutableList<NotificationUM>.addExistentialWarningNotification(
existentialDeposit: BigDecimal?,
feeAmount: BigDecimal,
receivedAmount: BigDecimal,
cryptoCurrencyStatus: CryptoCurrencyStatus,
onReduceClick: (
reduceAmountBy: BigDecimal,
reduceAmountByDiff: BigDecimal,
notification: Class<out NotificationUM>,
) -> Unit,
) {
val cryptoCurrency = cryptoCurrencyStatus.currency
val balance = cryptoCurrencyStatus.value.amount ?: return
val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) {
feeAmount
} else {
receivedAmount
}
val diff = balance.minus(spendingAmount)
if (existentialDeposit != null && diff >= BigDecimal.ZERO && existentialDeposit > diff) {
add(
NotificationUM.Error.ExistentialDeposit(
deposit = BigDecimalFormatter.formatCryptoAmountUncapped(
cryptoAmount = existentialDeposit,
cryptoCurrency = cryptoCurrency,
),
onConfirmClick = {
onReduceClick(
existentialDeposit,
existentialDeposit.minus(diff),
NotificationUM.Error.ExistentialDeposit::class.java,
)
},
),
)
}
}
fun MutableList<NotificationUM>.addFeeCoverageNotification(
isFeeCoverage: Boolean,
amountField: AmountFieldModel,
sendingValue: BigDecimal,
appCurrency: AppCurrency,
cryptoCurrencyStatus: CryptoCurrencyStatus,
) {
val cryptoCurrency = cryptoCurrencyStatus.currency
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val amountValue = amountField.cryptoAmount.value ?: return
val cryptoDiff = amountValue.minus(sendingValue)
if (isFeeCoverage) {
add(
NotificationUM.Warning.FeeCoverageNotification(
cryptoAmount = BigDecimalFormatter.formatCryptoAmountUncapped(
cryptoAmount = cryptoDiff,
cryptoCurrency = cryptoCurrency,
),
fiatAmount = getFiatString(
value = cryptoDiff,
rate = fiatRate,
appCurrency = appCurrency,
),
),
)
}
}
fun MutableList<NotificationUM>.addDustWarningNotification(
dustValue: BigDecimal?,
feeValue: BigDecimal,
sendingAmount: BigDecimal,
cryptoCurrencyStatus: CryptoCurrencyStatus,
feeCurrencyStatus: CryptoCurrencyStatus?,
) {
if (dustValue == null) return
val isExceedsLimit = checkDustLimits(
feeAmount = feeValue,
receivedAmount = sendingAmount,
dustValue = dustValue,
cryptoCurrencyStatus = cryptoCurrencyStatus,
feeCurrencyStatus = feeCurrencyStatus,
)
if (isExceedsLimit) {
add(
NotificationUM.Error.MinimumAmountError(
amount = dustValue.parseBigDecimal(cryptoCurrencyStatus.currency.decimals),
),
)
}
}
fun MutableList<NotificationUM>.addExceedsBalanceNotification(
cryptoCurrencyWarning: CryptoCurrencyWarning?,
cryptoCurrencyStatus: CryptoCurrencyStatus,
shouldMergeFeeNetworkName: Boolean,
onClick: (CryptoCurrency) -> Unit,
onAnalyticsEvent: (CryptoCurrency) -> Unit,
) {
when (cryptoCurrencyWarning) {
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> {
add(
NotificationUM.Error.ExceedsBalance(
networkIconId = cryptoCurrencyWarning.coinCurrency.networkIconResId,
networkName = cryptoCurrencyWarning.coinCurrency.name,
currencyName = cryptoCurrencyStatus.currency.name,
feeName = cryptoCurrencyWarning.coinCurrency.name,
feeSymbol = cryptoCurrencyWarning.coinCurrency.symbol,
mergeFeeNetworkName = shouldMergeFeeNetworkName,
onClick = {
onClick(cryptoCurrencyWarning.coinCurrency)
},
),
)
onAnalyticsEvent(cryptoCurrencyStatus.currency)
}
is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> {
val currency = cryptoCurrencyWarning.feeCurrency
add(
NotificationUM.Error.ExceedsBalance(
networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24,
currencyName = cryptoCurrencyWarning.currency.name,
feeName = cryptoCurrencyWarning.feeCurrencyName,
feeSymbol = cryptoCurrencyWarning.feeCurrencySymbol,
networkName = cryptoCurrencyWarning.networkName,
mergeFeeNetworkName = shouldMergeFeeNetworkName,
onClick = {
currency?.let {
onClick(currency)
}
},
),
)
onAnalyticsEvent(cryptoCurrencyWarning.currency)
}
else -> Unit
}
}
fun MutableList<NotificationUM>.addValidateTransactionNotifications(
dustValue: BigDecimal,
fee: Fee?,
validationError: Throwable?,
cryptoCurrency: CryptoCurrency,
onReduceClick: (
reduceAmountTo: BigDecimal,
notification: Class<out NotificationUM>,
) -> Unit,
) {
when (validationError) {
is BlockchainSdkError.Cardano -> addCardanoTransactionValidationError(
error = validationError,
sendingCurrency = cryptoCurrency,
dustValue = dustValue,
)
is BlockchainSdkError.Koinos -> addKoinosTransactionValidationError(
error = validationError,
onReduceClick = onReduceClick,
)
null -> (fee as? Fee.CardanoToken)?.let {
add(
NotificationUM.Cardano.MinAdaValueCharged(
tokenName = cryptoCurrency.name,
minAdaValue = it.minAdaValue.parseBigDecimal(cryptoCurrency.decimals),
),
)
}
else -> return
}
}
private fun MutableList<NotificationUM>.addCardanoTransactionValidationError(
error: BlockchainSdkError.Cardano,
sendingCurrency: CryptoCurrency,
dustValue: BigDecimal?,
) {
when (error) {
BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> {
add(NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name))
}
BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> {
when (sendingCurrency) {
is CryptoCurrency.Coin -> NotificationUM.Cardano.InsufficientBalanceToTransferCoin
is CryptoCurrency.Token -> {
NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name)
}
}.let(::add)
}
BlockchainSdkError.Cardano.InsufficientRemainingBalance,
BlockchainSdkError.Cardano.InsufficientSendingAdaAmount,
-> {
dustValue?.let {
add(
NotificationUM.Error.MinimumAmountError(
amount = it.parseBigDecimal(sendingCurrency.decimals),
),
)
}
}
}
}
private fun MutableList<NotificationUM>.addKoinosTransactionValidationError(
error: BlockchainSdkError.Koinos,
onReduceClick: (
reduceAmountTo: BigDecimal,
notification: Class<out NotificationUM>,
) -> Unit,
) {
when (error) {
is BlockchainSdkError.Koinos.InsufficientBalance -> {
add(NotificationUM.Koinos.InsufficientBalance)
}
is BlockchainSdkError.Koinos.InsufficientMana -> {
add(
NotificationUM.Koinos.InsufficientRecoverableMana(
mana = error.manaBalance ?: BigDecimal.ZERO,
maxMana = error.maxMana ?: BigDecimal.ZERO,
),
)
}
is BlockchainSdkError.Koinos.ManaFeeExceedsBalance -> {
add(
NotificationUM.Koinos.ManaExceedsBalance(
availableKoinForTransfer = error.availableKoinForTransfer,
onReduceClick = {
onReduceClick(
error.availableKoinForTransfer,
NotificationUM.Koinos.InsufficientRecoverableMana::class.java,
)
},
),
)
}
else -> {}
}
}
private fun checkDustLimits(
feeAmount: BigDecimal,
receivedAmount: BigDecimal,
dustValue: BigDecimal,
cryptoCurrencyStatus: CryptoCurrencyStatus,
feeCurrencyStatus: CryptoCurrencyStatus?,
): Boolean {
val change = when (cryptoCurrencyStatus.currency) {
is CryptoCurrency.Coin -> {
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
balance - (feeAmount + receivedAmount)
}
is CryptoCurrency.Token -> {
val balance = feeCurrencyStatus?.value?.amount ?: BigDecimal.ZERO
balance - feeAmount
}
}
val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO
return receivedAmount < dustValue || isChangeLowerThanDust
}
}

View file

@ -0,0 +1,69 @@
package com.tangem.common.ui.tokens
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
fun ScenarioUnavailabilityReason.getUnavailabilityReasonText(): TextReference {
return when (val unavailabilityReason = this) {
is ScenarioUnavailabilityReason.StakingUnavailable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_staking_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.PendingTransaction -> {
when (unavailabilityReason.withdrawalScenario) {
ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference(
id = R.string.token_button_unavailability_reason_pending_transaction_send,
formatArgs = wrappedList(unavailabilityReason.networkName),
)
ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference(
id = R.string.token_button_unavailability_reason_pending_transaction_sell,
formatArgs = wrappedList(unavailabilityReason.networkName),
)
}
}
is ScenarioUnavailabilityReason.EmptyBalance -> {
when (unavailabilityReason.withdrawalScenario) {
ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference(
id = R.string.token_button_unavailability_reason_empty_balance_send,
)
ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference(
id = R.string.token_button_unavailability_reason_empty_balance_sell,
)
}
}
is ScenarioUnavailabilityReason.BuyUnavailable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_buy_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.NotExchangeable -> {
resourceReference(
id = R.string.token_button_unavailability_reason_not_exchangeable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
is ScenarioUnavailabilityReason.NotSupportedBySellService -> {
resourceReference(
id = R.string.token_button_unavailability_reason_sell_unavailable,
formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName),
)
}
ScenarioUnavailabilityReason.Unreachable -> {
resourceReference(
id = R.string.token_button_unavailability_generic_description,
)
}
ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference(
id = R.string.warning_receive_blocked_hedera_token_association_required_message,
)
ScenarioUnavailabilityReason.None -> {
throw IllegalArgumentException("The unavailability reason must be other than None")
}
}
}

View file

@ -0,0 +1,193 @@
package com.tangem.common.ui.tokens
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
/**
* Token item state converter from [CryptoCurrencyStatus] to [TokenItemState]
*
* @property appCurrency app currency
* @property titleStateProvider title state provider
* @property subtitleStateProvider subtitle state provider
* @property onItemClick callback is invoked when item is clicked
* @property onItemLongClick callback is invoked when item is long clicked
*/
class TokenItemStateConverter(
private val appCurrency: AppCurrency,
private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = Companion::createTitleState,
private val subtitleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.SubtitleState? = {
createSubtitleState(it, appCurrency)
},
private val onItemClick: (CryptoCurrencyStatus) -> Unit,
private val onItemLongClick: ((CryptoCurrencyStatus) -> Unit)? = null,
) : Converter<CryptoCurrencyStatus, TokenItemState> {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
return when (value.value) {
is CryptoCurrencyStatus.Loading -> value.mapToLoadingState()
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> value.mapToTokenItemState()
is CryptoCurrencyStatus.MissedDerivation -> value.mapToNoAddressTokenItemState()
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> value.mapToUnreachableTokenItemState()
}
}
private fun CryptoCurrencyStatus.mapToLoadingState(): TokenItemState.Loading {
return TokenItemState.Loading(
id = currency.id.value,
iconState = iconStateConverter.convert(value = this),
titleState = titleStateProvider(this) as TokenItemState.TitleState.Content,
subtitleState = requireNotNull(subtitleStateProvider(this)),
)
}
private fun CryptoCurrencyStatus.mapToTokenItemState(): TokenItemState.Content {
return TokenItemState.Content(
id = currency.id.value,
iconState = iconStateConverter.convert(value = this),
titleState = titleStateProvider(this),
subtitleState = requireNotNull(subtitleStateProvider(this)),
fiatAmountState = TokenItemState.FiatAmountState.Content(
text = getFormattedFiatAmount(),
hasStaked = !getStakedBalance().isZero(),
),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()),
onItemClick = { onItemClick(this) },
onItemLongClick = onItemLongClick?.let {
{ it(this) }
},
)
}
private fun CryptoCurrencyStatus.getFormattedAmount(): String {
val amount = value.amount?.plus(getStakedBalance()) ?: return DASH_SIGN
return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals)
}
private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String {
val fiatYieldBalance = value.fiatRate?.times(getStakedBalance()).orZero()
val fiatAmount = value.fiatAmount?.plus(fiatYieldBalance) ?: return DASH_SIGN
return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol)
}
private fun CryptoCurrencyStatus.getStakedBalance() =
(value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero()
private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState(): TokenItemState.Unreachable {
return TokenItemState.Unreachable(
id = currency.id.value,
iconState = iconStateConverter.convert(value = this),
titleState = titleStateProvider(this),
subtitleState = subtitleStateProvider(this),
onItemClick = { onItemClick(this) },
onItemLongClick = onItemLongClick?.let {
{ it(this) }
},
)
}
private fun CryptoCurrencyStatus.mapToNoAddressTokenItemState(): TokenItemState.NoAddress {
return TokenItemState.NoAddress(
id = currency.id.value,
iconState = iconStateConverter.convert(this),
titleState = titleStateProvider(this),
subtitleState = subtitleStateProvider(this),
onItemLongClick = onItemLongClick?.let {
{ it(this) }
},
)
}
private companion object {
fun createTitleState(currencyStatus: CryptoCurrencyStatus): TokenItemState.TitleState {
return when (val value = currencyStatus.value) {
is CryptoCurrencyStatus.Loading,
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> {
TokenItemState.TitleState.Content(text = currencyStatus.currency.name)
}
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> {
TokenItemState.TitleState.Content(
text = currencyStatus.currency.name,
hasPending = value.hasCurrentNetworkTransactions,
)
}
}
}
fun createSubtitleState(
currencyStatus: CryptoCurrencyStatus,
appCurrency: AppCurrency,
): TokenItemState.SubtitleState? {
return when (currencyStatus.value) {
is CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> currencyStatus.getCryptoPriceState(appCurrency)
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> null
}
}
private fun CryptoCurrencyStatus.getCryptoPriceState(appCurrency: AppCurrency): TokenItemState.SubtitleState {
val fiatRate = value.fiatRate
val priceChange = value.priceChange
return if (fiatRate != null && priceChange != null) {
TokenItemState.SubtitleState.CryptoPriceContent(
price = fiatRate.getFormattedCryptoPrice(appCurrency),
priceChangePercent = BigDecimalFormatter.formatPercent(
percent = priceChange,
useAbsoluteValue = true,
),
type = priceChange.getPriceChangeType(),
)
} else {
TokenItemState.SubtitleState.Unknown
}
}
private fun BigDecimal.getFormattedCryptoPrice(appCurrency: AppCurrency): String {
return BigDecimalFormatter.formatFiatAmountUncapped(
fiatAmount = this,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}
private fun BigDecimal.getPriceChangeType(): PriceChangeType {
return PriceChangeConverter.fromBigDecimal(value = this)
}
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.common.ui.tokens
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.res.TangemTheme
/**
* Text view for token price.
*
* @param price Price of the token.
* @param priceChangeType Type of the price change.
*/
@Composable
fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) {
val growColor = TangemTheme.colors.text.accent
val fallColor = TangemTheme.colors.text.warning
val generalColor = TangemTheme.colors.text.primary1
val color = remember(generalColor) { Animatable(generalColor) }
var animationSkipped by remember { mutableStateOf(false) }
LaunchedEffect(price) {
if (animationSkipped.not()) {
animationSkipped = true
return@LaunchedEffect
}
if (priceChangeType != null) {
val nextColor = when (priceChangeType) {
PriceChangeType.UP,
-> growColor
PriceChangeType.DOWN -> fallColor
PriceChangeType.NEUTRAL -> return@LaunchedEffect
}
color.animateTo(nextColor, snap())
color.animateTo(generalColor, tween(durationMillis = 500))
}
}
Text(
modifier = modifier,
text = price,
color = color.value,
maxLines = 1,
style = TangemTheme.typography.body2,
overflow = TextOverflow.Visible,
)
}

View file

@ -0,0 +1,213 @@
package com.tangem.common.ui.userwallet
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CardColors
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.components.block.TangemBlockCardColors
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,
blockColors: CardColors = TangemBlockCardColors,
) {
BlockCard(
modifier = modifier,
colors = blockColors,
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
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@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(
Modifier
.background(TangemTheme.colors.background.tertiary)
.padding(TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
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,104 @@
package com.tangem.common.ui.userwallet.converter
import com.tangem.common.ui.R
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.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.utils.StringsSigns.DOT
import com.tangem.utils.StringsSigns.STARS
import com.tangem.utils.converter.Converter
/**
* Converter from [UserWallet] to [UserWalletItemUM]
*
* @property onClick lambda be invoked when item is clicked
* @property appCurrency selected app currency
* @property balance wallet balance
* @property isLoading wallet loading state
* @property isBalanceHidden wallet balance is hidden
*
[REDACTED_AUTHOR]
*/
class UserWalletItemUMConverter(
private val onClick: (UserWalletId) -> Unit,
private val appCurrency: AppCurrency? = null,
private val balance: TotalFiatBalance? = null,
private val isLoading: Boolean = true,
private val isBalanceHidden: Boolean = false,
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
) : Converter<UserWallet, UserWalletItemUM> {
override fun convert(value: UserWallet): UserWalletItemUM {
return with(value) {
UserWalletItemUM(
id = walletId,
name = stringReference(name),
information = getInfo(
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = isBalanceHidden,
isLoading = isLoading,
),
imageUrl = artworkUrl,
isEnabled = !isLocked,
endIcon = endIcon,
onClick = { onClick(value.walletId) },
)
}
}
private fun UserWallet.getInfo(
appCurrency: AppCurrency?,
balance: TotalFiatBalance?,
isBalanceHidden: Boolean,
isLoading: Boolean,
): TextReference {
val dividerRef = stringReference(value = " $DOT ")
val cardCount = getCardsCount() ?: 1
val cardCountRef = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
return when {
isBalanceHidden -> combinedReference(cardCountRef, dividerRef, stringReference(STARS))
isLocked -> combinedReference(cardCountRef, dividerRef, resourceReference(R.string.common_locked))
isLoading -> cardCountRef
else -> getBalanceInfo(balance, appCurrency, cardCountRef, dividerRef)
}
}
private fun getBalanceInfo(
balance: TotalFiatBalance?,
appCurrency: AppCurrency?,
cardCountRef: TextReference,
dividerRef: TextReference,
): TextReference {
val amount = when (balance) {
is TotalFiatBalance.Loaded -> balance.amount
is TotalFiatBalance.Failed,
is TotalFiatBalance.Loading,
null,
-> null
}
return if (amount != null && appCurrency != null) {
val formattedAmount = BigDecimalFormatter.formatFiatAmount(
fiatAmount = amount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
val amountRef = stringReference(formattedAmount)
combinedReference(cardCountRef, dividerRef, amountRef)
} else {
combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN))
}
}
}

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

@ -0,0 +1,8 @@
package com.tangem.core.analytics
interface AppInstanceIdProvider {
suspend fun getAppInstanceId(): String?
fun getAppInstanceIdSync(): String?
}

View file

@ -0,0 +1,12 @@
package com.tangem.core.analytics
class DummyAppInstanceIdProvider : AppInstanceIdProvider {
override suspend fun getAppInstanceId(): String? {
return null
}
override fun getAppInstanceIdSync(): String? {
return null
}
}

View file

@ -67,7 +67,16 @@ data class TokenMarketInfoResponse(
val buyPressureChange: Change?,
@Json(name = "experienced_buyer_change")
val experiencedBuyerChange: Change?,
)
@Json(name = "networks")
val sourceNetworks: List<SourceNetwork>?,
) {
data class SourceNetwork(
@Json(name = "network_id")
val id: String,
@Json(name = "network_name")
val name: String,
)
}
data class Change(
@Json(name = "24h")

View file

@ -17,6 +17,7 @@ interface StakeKitApi {
@GET("yields/enabled")
suspend fun getMultipleYields(
@Query("preferredValidatorsOnly") preferredValidatorsOnly: Boolean? = null,
@Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean? = null,
@Query("type") type: YieldType? = null,
@Query("revenueOption") revenueOption: RevenueOption? = null,
@ -34,7 +35,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

@ -48,7 +48,7 @@ data class ActionRequestBodyArgs(
@Json(name = "ledgerWalletAPICompatible")
val ledgerWalletAPICompatible: Boolean? = null,
@Json(name = "tronResource")
val tronResource: String? = null,
val tronResource: TronResource? = null,
@Json(name = "signatureVerification")
val signatureVerification: SignatureVerification? = null,
@Json(name = "inputToken")
@ -60,4 +60,12 @@ data class SignatureVerification(
val message: String,
@Json(name = "signed")
val signed: String,
)
)
enum class TronResource {
@Json(name = "ENERGY")
ENERGY,
@Json(name = "BANDWIDTH")
BANDWIDTH,
}

View file

@ -2,12 +2,15 @@ package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.stakekit.models.request.Address
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO
import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class YieldBalanceWrapperDTO(
@Json(name = "addresses")
val addresses: Address,
@Json(name = "balances")
val balances: List<BalanceDTO>,
@Json(name = "integrationId")

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.stakekit.models.response.model.transaction.tron
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class TronStakeKitTransaction(
@Json(name = "raw_data_hex")
val rawDataHex: String,
)

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.tangem.common.extensions.calculateHashCode
data class UserTokensResponse(
@Json(name = "version") val version: Int = 0,
@ -17,7 +18,23 @@ data class UserTokensResponse(
@Json(name = "symbol") val symbol: String,
@Json(name = "decimals") val decimals: Int,
@Json(name = "contractAddress") val contractAddress: String?,
)
) {
override fun equals(other: Any?): Boolean {
val otherToken = other as? Token ?: return false
return otherToken.contractAddress == this.contractAddress &&
otherToken.networkId == this.networkId &&
otherToken.derivationPath == this.derivationPath &&
otherToken.decimals == this.decimals
}
override fun hashCode(): Int = calculateHashCode(
contractAddress.hashCode(),
networkId.hashCode(),
derivationPath.hashCode(),
decimals.hashCode(),
)
}
enum class GroupType {
@Json(name = "none")

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

@ -1,51 +1,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.flow.mapNotNull
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)
.map { balances ->
balances.filter { it.integrationId == integrationId }
.flatMap { it.balances }
override fun get(userWalletId: UserWalletId, integrationId: String): Flow<YieldBalanceWrapperDTO> {
return dataStore.get(userWalletId.stringValue)
.mapNotNull { balances ->
balances.firstOrNull { it.integrationId == integrationId }
}
}
override suspend fun getSyncOrNull(integrationId: String): List<BalanceDTO>? {
return dataStore.getSyncOrNull(STAKING_BALANCE_KEY)
?.firstOrNull { it.integrationId == integrationId }?.balances
override suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): YieldBalanceWrapperDTO? {
return dataStore.getSyncOrNull(userWalletId.stringValue)
?.firstOrNull { it.integrationId == integrationId }
}
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

@ -1,20 +1,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<YieldBalanceWrapperDTO>
suspend fun getSyncOrNull(integrationId: String): List<BalanceDTO>?
suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): YieldBalanceWrapperDTO?
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

@ -19,6 +19,7 @@ class DefaultAppComponentContext(
messageHandler: UiMessageHandler,
override val dispatchers: CoroutineDispatcherProvider,
override val hiltComponentBuilder: DecomposeComponent.Builder,
private val replaceRouter: Router? = null,
) : AppComponentContext, ComponentContext by componentContext {
override val tags: HashMap<String, Any> = HashMap()
@ -31,5 +32,5 @@ class DefaultAppComponentContext(
get() = instanceKeeper.getOrCreate { DefaultAppNavigationProvider() }
override val router: Router
get() = instanceKeeper.getOrCreate { DefaultRouter(navigationProvider) }
get() = replaceRouter ?: instanceKeeper.getOrCreate { DefaultRouter(navigationProvider) }
}

View file

@ -46,14 +46,36 @@ abstract class Model : InstanceKeeper.Instance {
progressFlow: MutableSharedFlow<Boolean>,
dispatcher: CoroutineDispatcher = dispatchers.mainImmediate,
crossinline block: suspend () -> Unit,
): Job = resource(
acquire = { progressFlow.emit(true) },
release = { progressFlow.emit(false) },
dispatcher = dispatcher,
block = block,
)
/**
* Launches [block] in the model's scope and acquires a resource before executing the block and releases it after.
*
* @param acquire The block of code to acquire the resource.
* @param release The block of code to release the resource.
* @param dispatcher The [CoroutineDispatcher] to launch the coroutine. Default is [Dispatchers.Main.immediate].
* @param block The block of code to execute.
*
* @return The [Job] of the launched coroutine.
* */
protected inline fun resource(
crossinline acquire: suspend () -> Unit,
crossinline release: suspend () -> Unit,
dispatcher: CoroutineDispatcher = dispatchers.mainImmediate,
crossinline block: suspend () -> Unit,
): Job = modelScope.launch(dispatcher) {
progressFlow.emit(value = true)
acquire()
try {
block()
} finally {
withContext(NonCancellable) {
progressFlow.emit(value = false)
release()
}
}
}

View file

@ -100,10 +100,10 @@
<string name="common_enable">Aktivieren</string>
<string name="common_enabled">Aktiviert</string>
<string name="common_error">Fehler</string>
<string name="common_exchange">Umtausch</string>
<string name="common_explore">Erkunden</string>
<string name="common_explore_transaction_history">Transaktionsverlauf einsehen</string>
<string name="common_explorer">Explorer</string>
<string name="common_fee_label">Gebühr</string>
<string name="common_fee_selector_footer">Netzwerkgebühren sind Gebühren, die Nutzer für die Verarbeitung und Bestätigung von Transaktionen zahlen. Die Höhe der Gebühren kann von der Überlastung des Netzes, der Größe der Transaktion und der Ausführungspriorität abhängen. %s</string>
<string name="common_fee_selector_option_fast">Schnell</string>
<string name="common_fee_selector_option_market">Markt</string>
@ -113,6 +113,10 @@
<string name="common_go_to_provider">Zum Anbieter gehen</string>
<string name="common_go_to_token">Zum Token</string>
<string name="common_import">Importieren</string>
<plurals name="common_in_days">
<item quantity="one">in %d Tag</item>
<item quantity="other">in %d Tagen</item>
</plurals>
<string name="common_later">Später</string>
<string name="common_locked">Gesperrt</string>
<string name="common_main_network">Hauptnetz</string>
@ -168,7 +172,9 @@
<string name="custom_token_contract_address_input_title">Vertragsadresse</string>
<string name="custom_token_creation_error_invalid_contract_address">Vertragsadresse ist ungültig</string>
<string name="custom_token_creation_error_network_not_selected">Bitte wähle das Netzwerk</string>
<string name="custom_token_creation_error_wrong_decimals">Dezimalzahl muss eine gültige Ganzzahl sein, bis zu %li</string>
<string name="custom_token_creation_error_token_already_exist_message">Dieses Token wurde bereits zur Liste hinzugefügt</string>
<string name="custom_token_creation_error_token_already_exist_title">Token existiert bereits</string>
<string name="custom_token_creation_error_wrong_decimals">Dezimalzahl muss eine gültige Ganzzahl sein, bis zu %d</string>
<string name="custom_token_custom_derivation">Benutzerdefinierte Ableitung(derivation)</string>
<string name="custom_token_custom_derivation_placeholder">E. g. m/00\'/0000\'/0\'/0/0</string>
<string name="custom_token_custom_derivation_title">Benutzerdefinierte Ableitung (Derivation) eingeben</string>
@ -338,12 +344,14 @@
<string name="markets_add_to_my_portfolio_description">Um mit dem Kauf, Tausch oder Erhalt dieses Vermögenswerts zu beginnen, füge diesen Token zu mindestens 1 Netzwerk hinzu</string>
<string name="markets_add_to_my_portfolio_unavailable_description">Dieses Asset ist nicht verfügbar</string>
<string name="markets_add_to_portfolio_button">Zum Portfolio hinzufügen</string>
<string name="markets_add_token">Token hinzufügen</string>
<string name="markets_add_token">Hinzufügen</string>
<string name="markets_available_networks">Verfügbare Netzwerke</string>
<string name="markets_common_my_portfolio">Mein Portfolio</string>
<string name="markets_common_title">Markt</string>
<string name="markets_generate_addresses_notification">Um Adressen für ausgewählte Netzwerke zu generieren, musst du eine Tangem-Karte scannen</string>
<string name="markets_insights_info_description_message">Die Daten dieses Abschnitts stammen aus den folgenden Netzwerken: %s</string>
<string name="markets_loading_error_title">Die Daten konnten nicht geladen werden...</string>
<string name="markets_loading_no_data_title">Keine Daten</string>
<string name="markets_quick_actions">Schnelle Aktionen</string>
<string name="markets_search_result_title">Ergebnis</string>
<string name="markets_search_see_tokens_under_100k">Token unter 100k Marktkapitalisierung anzeigen</string>
@ -369,7 +377,7 @@
<item quantity="one">Bewertung, basierend auf %d</item>
<item quantity="other">Bewertungen, basierend auf %d</item>
</plurals>
<string name="markets_token_details_blockchain_site">Blockchain-Site</string>
<string name="markets_token_details_blockchain_site">Webseite</string>
<string name="markets_token_details_buy_pressure">Kaufdruck</string>
<string name="markets_token_details_buy_pressure_description">Die Differenz zwischen Käufer- und Verkäufervolumen</string>
<string name="markets_token_details_circulating_supply">Umlaufmenge</string>
@ -465,7 +473,7 @@
<string name="onboarding_seed_intro_title">Seed-Phrase verwenden</string>
<string name="onboarding_seed_mnemonic_invalid_checksum">Ungültige Seed-Phrase. Bitte überprüfe die Wortreihenfolge.</string>
<string name="onboarding_seed_mnemonic_wrong_words">Ungültige Seed-Phrase. Bitte überprüfe die Rechtschreibung.</string>
<string name="onboarding_seed_phrase_intro_legacy">veralteter Standard</string>
<string name="onboarding_seed_phrase_intro_legacy">Veralteter Standard</string>
<string name="onboarding_seed_user_validation_message">Um zu überprüfen, ob du deine Seed-Phrase richtig aufgeschrieben hast, gib bitte das 2., 7. und 11 Wort ein.</string>
<string name="onboarding_seed_user_validation_title">Eine letzte Prüfung!</string>
<string name="onboarding_subtitle_no_backup_cards">Um den Sicherungsvorgang zu starten, füge bis zu zwei Sicherungskarten hinzu.</string>
@ -571,18 +579,11 @@
<string name="send_custom_kaspa_per_utxo_footer">Die Gebühr, die für die Nutzung jeder nicht ausgegebenen Transaktionsausgabe (UTXO) im Kaspa-Netzwerk erforderlich ist. Je mehr UTXOs du in einer Transaktion verwenden, desto höher ist die Gebühr.</string>
<string name="send_custom_kaspa_per_utxo_title">KAS per UTXO</string>
<string name="send_date_format">%1$s, %2$s</string>
<string name="send_destination_hint_address">Adresse</string>
<string name="send_destination_tag_field">Ziel-Tag</string>
<string name="send_enter_address_field">Adresse eingeben</string>
<string name="send_error_address_same_as_wallet">Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein</string>
<string name="send_extras_error_invalid_destination_tag">Ungültiges Tag. Es wird der Transaktion nicht hinzugefügt.</string>
<string name="send_extras_error_invalid_memo">Ungültiges Memo. Sie wird der Transaktion nicht hinzugefügt.</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_fee_include_description">inkl. Gebühr</string>
<string name="send_fee_picker_low">Niedrig</string>
<string name="send_fee_picker_normal">Normal</string>
<string name="send_fee_picker_priority">Priorität</string>
<string name="send_fee_unreachable_error_text">Überprüfe deine Netzwerkverbindung</string>
<string name="send_fee_unreachable_error_title">Informationen zur Netzwerkgebühr nicht erreichbar</string>
<string name="send_from_wallet_android">Von</string>
@ -597,7 +598,7 @@
<string name="send_network_fee_warning_title">Abdeckung der Netzgebühren</string>
<string name="send_notification_exceed_balance_text">Unzureichende Mittel für die Überweisung, da die Summe aus Gebühr und Überweisungsbetrag das bestehende Guthaben übersteigt</string>
<string name="send_notification_exceed_balance_title">Gesamtbetrag übersteigt den Saldo</string>
<string name="send_notification_existential_deposit_text">Das Konto wird von der Blockchain gelöscht, wenn der Kontostand unter die Mindesteinlage fällt. Bitte belasse %s auf deinem Konto.</string>
<string name="send_notification_existential_deposit_text">Ein Guthaben von mindestens %s ist erforderlich, um dein Konto in der Blockchain aktiv zu halten und Sicherheitsrisiken zu vermeiden. Dieser Betrag verbleibt auf deinem Guthaben und kann nicht abgehoben werden.</string>
<string name="send_notification_existential_deposit_title">Mindesteinlage</string>
<string name="send_notification_fee_too_high_text">Der Kommissionsbetrag ist %s mal der empfohlene Betrag. Stelle sicher, dass die benutzerdefinierten Einstellungen korrekt sind.</string>
<string name="send_notification_fee_too_high_title">Die individuelle Gebühr ist hoch</string>
@ -631,21 +632,15 @@
<string name="send_summary_title">Versende %s</string>
<string name="send_summary_transaction_description">Du sendest **%1$s** inklusive der Netzwerkgebühr %2$s</string>
<string name="send_summary_transaction_description_no_fiat_fee">Du sendest **%1$s** und %2$s</string>
<string name="send_title_currency_format">Senden %s</string>
<string name="send_total_label">Gesamt</string>
<string name="send_total_subtitle_asset_format">%1$s und %2$s werden gesendet</string>
<string name="send_total_subtitle_fiat_format">≈ %1$s (inkl. Gebühr: %2$s)</string>
<string name="send_total_subtitle_format">%s wird gesendet</string>
<string name="send_transaction_success">Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert</string>
<string name="send_tron_account_activation_error">%1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen.</string>
<string name="send_validation_invalid_address">Ungültige Adresse</string>
<string name="sent_transaction_sent_title">Transaktion gesendet</string>
<string name="settings_card_settings_footer">Bereite das Scannen der Karte vor, die du einrichten möchtest.</string>
<string name="settings_forget_wallet">Entferne diese Wallet</string>
<string name="settings_forget_wallet_footer">Hiermit wird die Wallet aus der Anwendung entfernt. Die Wallet selbst kann wieder hinzugefügt werden.</string>
<string name="settings_wallet_name_title">Name</string>
<string name="staking_active">Aktiv</string>
<string name="staking_active_footer">Um deine Kryptos zu unstaken, klick hier.</string>
<string name="staking_active_footer">Um deine Vermögenswerte freizugeben, tippe auf den Block oben</string>
<string name="staking_amount_requirement_error">Die Anzahl der zu stakenden Krypros muss mindesten %s betragen</string>
<string name="staking_claim_unstaked">Nicht gestakte beanspruche</string>
<string name="staking_details_annual_percentage_rate">Jährliche prozentuale Rendite</string>
@ -658,12 +653,12 @@
<string name="staking_details_market_rating">Marktbewertung</string>
<string name="staking_details_metrics_block_header">Metriken</string>
<string name="staking_details_minimum_requirement">Mindestanforderungen</string>
<string name="staking_details_no_rewards_to_claim">Keine Belohnungen zu beanspruchen.</string>
<string name="staking_details_no_rewards_to_claim">Keine Belohnungen</string>
<string name="staking_details_reward_claiming">Belohnungen beanspruchen</string>
<string name="staking_details_reward_claiming_info">Eine Möglichkeit, Staking-Belohnungen zu erhalten. Es kann automatisch oder manuell beansprucht werden.</string>
<string name="staking_details_reward_schedule">Belohnungszeitplan</string>
<string name="staking_details_reward_schedule_info">Dabei handelt es sich um einen Zeitplan, der festlegt, wann die Teilnehmer am Staking ihre Belohnungen erhalten.</string>
<string name="staking_details_rewards_to_claim">Belohnungen, die du beanspruchen kannst: %s</string>
<string name="staking_details_rewards_to_claim">Belohnungen %s</string>
<string name="staking_details_title">Staking %s</string>
<string name="staking_details_unbonding_period">Entbindungsdauer</string>
<string name="staking_details_unbonding_period_info">Der Zeitraum, den du nach der Beantragung der Abhebung von Geldern aus dem Staking warten musst, bevor die Token verfügbar werden.</string>
@ -677,6 +672,7 @@
<string name="staking_notification_earn_rewards_text_period_week">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche.</string>
<string name="staking_notification_earn_rewards_title">Verdiene Staking-Belohnungen</string>
<string name="staking_notification_unstake_text">Die Belohnungen werden sofort nach dem unstaken gestoppt. Der unstakingprozess dauert %s.</string>
<string name="staking_ready_to_withdraw">Bereit zum Abheben</string>
<string name="staking_rebond">Erneut binden</string>
<string name="staking_restake">Erneut staken</string>
<string name="staking_restake_rewards">Belohnungen erneut staken</string>
@ -697,6 +693,7 @@
<string name="staking_stake_more">Mehr staken</string>
<string name="staking_title_stake">Stake %s</string>
<string name="staking_title_unstake">Staking beenden%s</string>
<string name="staking_unbonding">Lösen der Bindungen</string>
<string name="staking_unlocked_locked">Gelocktes unlocken</string>
<string name="staking_unstaked">Unstaken</string>
<string name="staking_unstaked_footer">Prüfe, was nicht eingesetzt wurde, um dein Vermögen zu beanspruchen</string>
@ -834,6 +831,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">Das Genehmigungsverfahren ist derzeit im Gange und wird in Kürze abgeschlossen sein</string>
<string name="warning_approval_in_progress_title">Genehmigung läuft</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>
@ -875,8 +874,8 @@
<string name="warning_low_signatures_message">Auf dieser Karte sind nur noch %s Unterschriften übrig. Du musst dein gesamtes Guthaben abheben.</string>
<string name="warning_low_signatures_title">Geringe Anzahl von Unterschriften</string>
<string name="warning_manage_tokens_legacy_derivation_message">Token in verschiedenen Netzwerken können unterschiedliche Adressen haben. Überprüfe bei der Überweisung noch einmal, ob deine Adresse mit der des Netzwerks übereinstimmt.</string>
<string name="warning_matic_migration_title">Migration von MATIC zu POL</string>
<string name="warning_matic_migration_message">MATIC wird auf POL migriert. Es gibt jedoch keine Frist, und MATIC wird noch nicht abgeschafft. Du kannst MATIC-Token weiterhin verwenden oder sie über eien Exchange gegen POL tauschen.</string>
<string name="warning_matic_migration_title">Migration von MATIC zu POL</string>
<plurals name="warning_missing_derivation_message">
<item quantity="one">Verwende deine Karte, um eine Adresse für das %d-Netz zu erhalten</item>
<item quantity="other">Verwende deine Karte, um mehrere Adressen für die %d-Netzwerke zu erhalten</item>

View file

@ -101,7 +101,6 @@
<string name="common_explore">Explorez</string>
<string name="common_explore_transaction_history">Explorez l\'historique des transactions</string>
<string name="common_explorer">Explorateur</string>
<string name="common_fee_label">Commissions</string>
<string name="common_fee_selector_footer">Les frais de réseau sont des charges que les utilisateurs paient pour traiter et confirmer les transactions. Le montant des frais peut être affecté par la congestion du réseau, la taille de la transaction et la priorité d\'exécution. %s</string>
<string name="common_fee_selector_option_fast">Rapide</string>
<string name="common_fee_selector_option_market">Marché</string>
@ -164,7 +163,7 @@
<string name="custom_token_contract_address_input_title">Adresse du contrat</string>
<string name="custom_token_creation_error_invalid_contract_address">L\'adresse du contrat n\'est pas valide</string>
<string name="custom_token_creation_error_network_not_selected">Veuillez sélectionner le réseau</string>
<string name="custom_token_creation_error_wrong_decimals">Les décimales doivent être un entier valide, jusqu\'à %li</string>
<string name="custom_token_creation_error_wrong_decimals">Les décimales doivent être un entier valide, jusqu\'à %d</string>
<string name="custom_token_custom_derivation">Dérivation personnalisée</string>
<string name="custom_token_custom_derivation_placeholder">Par exemple m/00\'/0000\'/0\'/0/0</string>
<string name="custom_token_custom_derivation_title">Entrez une dérivation personnalisée</string>
@ -508,18 +507,11 @@
<string name="send_custom_kaspa_per_utxo_footer">Les frais requis pour l\'utilisation de chaque sortie de transaction non dépensée (UTXO) dans le réseau Kaspa. Plus vous utilisez dUTXO dans une transaction, plus les frais seront élevés.</string>
<string name="send_custom_kaspa_per_utxo_title">KAS pour UTXO</string>
<string name="send_date_format">%1$s, %2$s</string>
<string name="send_destination_hint_address">Adresse</string>
<string name="send_destination_tag_field">ID de destination</string>
<string name="send_enter_address_field">Entrez l\'adresse</string>
<string name="send_error_address_same_as_wallet">L\'adresse est la même que celle de votre portefeuille</string>
<string name="send_extras_error_invalid_destination_tag">Tag invalide. Il ne sera pas ajouté à la transaction.</string>
<string name="send_extras_error_invalid_memo">Mémo invalide. Il ne sera pas ajouté à la transaction.</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_fee_include_description">Inclure les commissions</string>
<string name="send_fee_picker_low">Bas</string>
<string name="send_fee_picker_normal">Normal</string>
<string name="send_fee_picker_priority">Priorité</string>
<string name="send_fee_unreachable_error_text">Vérifiez votre connexion réseau</string>
<string name="send_fee_unreachable_error_title">Informations sur les frais de réseau inaccessibles</string>
<string name="send_from_wallet_android">De</string>
@ -534,7 +526,7 @@
<string name="send_network_fee_warning_title">Couverture des frais de réseau</string>
<string name="send_notification_exceed_balance_text">Fonds insuffisants pour le transfert, car le total des frais et du montant du transfert dépasse le solde existant</string>
<string name="send_notification_exceed_balance_title">Le total dépasse le solde</string>
<string name="send_notification_existential_deposit_text">Le compte sera effacé de la blockchain si un solde descend en dessous du dépôt existentiel. Veuillez en laisser %s sur votre solde.</string>
<string name="send_notification_existential_deposit_text">Un solde d\'au moins %s est requis pour conserver votre compte sur la blockchain afin d\'éviter les risques de sécurité. Ce montant restera sur votre solde et ne pourra pas être retiré.</string>
<string name="send_notification_existential_deposit_title">Dépôt existentiel</string>
<string name="send_notification_fee_too_high_text">Le montant de la commission est %s fois le montant recommandé. Assurez-vous que les paramètres personnalisés sont corrects.</string>
<string name="send_notification_fee_too_high_title">Les frais de douane sont élevés</string>
@ -568,21 +560,16 @@
<string name="send_summary_title">Envoyer %s</string>
<string name="send_summary_transaction_description">Vous envoyez **%1$s** incluant des frais de réseau de %2$s</string>
<string name="send_summary_transaction_description_no_fiat_fee">Vous envoyez **%1$s** et %2$s</string>
<string name="send_title_currency_format">Envoi de %s</string>
<string name="send_total_label">Total</string>
<string name="send_total_subtitle_asset_format">Sera envoyé %1$s et %2$s</string>
<string name="send_total_subtitle_fiat_format">≈ %1$s (incl. les commissions : %2$s)</string>
<string name="send_total_subtitle_format">Sera envoyé %s</string>
<string name="send_transaction_success">La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps</string>
<string name="send_tron_account_activation_error">%1$s est un actif du réseau Tron. Pour calculer les frais et effectuer une transaction, déposez du Tron (TRX) sur votre compte.</string>
<string name="send_validation_invalid_address">Adresse incorrecte</string>
<string name="sent_transaction_sent_title">Transaction envoyée</string>
<string name="settings_card_settings_footer">Scannez la carte que vous souhaitez configurer.</string>
<string name="settings_forget_wallet">Oublier le portefeuille</string>
<string name="settings_forget_wallet_footer">Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau.</string>
<string name="settings_wallet_name_title">Nom</string>
<string name="staking_active">Actif</string>
<string name="staking_active_footer">Afin d\'unstaker vos actifs, cliquez ici.</string>
<string name="staking_active_footer">Afin d\'unstaker vos actifs, appuyez sur le bloc ci-dessus</string>
<string name="staking_details_annual_percentage_rate">Pourcentage de rendement annuel</string>
<string name="staking_details_annual_percentage_rate_info">Le pourcentage de rendement annuel que vous pouvez gagner en participant au staking.</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_available">Disponible</string>
@ -604,6 +591,7 @@
<string name="staking_details_warmup_period_info">Le temps imparti pour activer la participation au staking.</string>
<string name="staking_native">Native staking</string>
<string name="staking_notification_earn_rewards_title">Gagnez des récompenses de staking</string>
<string name="staking_notification_unstake_text">Les récompenses cessent de s\'accumuler immédiatement après que vous ayez commencé le unstaking. Le processus de unstaking prend %s.</string>
<string name="staking_rewards">Récompenses</string>
<string name="staking_stake_locked">Stake verrouillé</string>
<string name="staking_stake_more">Staker plus</string>

View file

@ -8,7 +8,6 @@
<string name="common_delete">Rimuovere</string>
<string name="common_done">Fatto</string>
<string name="common_error">Errore</string>
<string name="common_fee_label">Commissione</string>
<string name="common_network_fee_title">Costi della rete</string>
<string name="common_ok">OK</string>
<string name="common_save_changes">Mantieni le modifiche</string>
@ -35,21 +34,10 @@
<string name="initial_message_tap_header">Avvicina la carta</string>
<string name="onboarding_create_wallet_button_create_wallet">Crea portafoglio</string>
<string name="send_amount_label">Importo</string>
<string name="send_destination_hint_address">Indirizzo</string>
<string name="send_error_address_same_as_wallet">L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_fee_include_description">Includi commissione</string>
<string name="send_fee_picker_low">Insufficiente</string>
<string name="send_fee_picker_normal">Normale</string>
<string name="send_fee_picker_priority">Prioritario</string>
<string name="send_max_amount_label">Importo totale</string>
<string name="send_total_label">Totale</string>
<string name="send_total_subtitle_asset_format">Sarà inviato %1$s e %2$s</string>
<string name="send_total_subtitle_fiat_format">≈ %1$s (inc. commissione: %2$s)</string>
<string name="send_total_subtitle_format">Sarà inviato %s</string>
<string name="send_transaction_success">La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo</string>
<string name="send_validation_invalid_address">Indirizzo non valido</string>
<string name="twins_recreate_toolbar">Tangem Twin</string>
<string name="wallet_connect_title">WalletConnect</string>
<string name="wallet_notification_address_copied">L\'indirizzo è stato copiato con successo</string>

View file

@ -39,7 +39,7 @@
<string name="biometric_lockout_warning_title">試行回数が多すぎます</string>
<string name="biometric_unavailable_warning">お使いの携帯電話で生体認証が無効になっているため、アプリにウォレットを保存できません。ウォレットを保存するには、携帯電話の設定で生体認証機能を有効にしてください。</string>
<string name="button_start_backup_process">バックアップ処理を開始する</string>
<string name="buy_token_description">法定通貨カードまたは銀行口座から</string>
<string name="buy_token_description">銀行カードまたは銀行口座から</string>
<plurals name="card_label_card_count">
<item quantity="other">%d カード</item>
</plurals>
@ -98,10 +98,10 @@
<string name="common_enable">有効にする</string>
<string name="common_enabled">有効</string>
<string name="common_error">エラー</string>
<string name="common_exchange">交換</string>
<string name="common_explore">移動する</string>
<string name="common_explore_transaction_history">取引履歴を調べる</string>
<string name="common_explorer">エクスプローラー</string>
<string name="common_fee_label">手数料</string>
<string name="common_fee_selector_footer">ネットワーク手数料は、取引の処理と確認のためにユーザーが支払う料金です。手数料の額は、ネットワークの混雑さ、取引のサイズ、実行の優先度によって左右されます。 %s</string>
<string name="common_fee_selector_option_fast">速い</string>
<string name="common_fee_selector_option_market">マーケット</string>
@ -111,6 +111,9 @@
<string name="common_go_to_provider">プロバイダーへ移動</string>
<string name="common_go_to_token">トークンへ移動</string>
<string name="common_import">インポート</string>
<plurals name="common_in_days">
<item quantity="other">%d日で</item>
</plurals>
<string name="common_later">後で</string>
<string name="common_locked">ロックされています</string>
<string name="common_main_network">メインネットワーク</string>
@ -166,7 +169,9 @@
<string name="custom_token_contract_address_input_title">コントラクトアドレス</string>
<string name="custom_token_creation_error_invalid_contract_address">コントラクトアドレスが無効です</string>
<string name="custom_token_creation_error_network_not_selected">ネットワークを選択してください</string>
<string name="custom_token_creation_error_wrong_decimals">小数は%liまでの有効な整数である必要があります</string>
<string name="custom_token_creation_error_token_already_exist_message">このトークンはすでにリストに追加されています</string>
<string name="custom_token_creation_error_token_already_exist_title">トークンはすでに存在します</string>
<string name="custom_token_creation_error_wrong_decimals">小数は%dまでの有効な整数である必要があります</string>
<string name="custom_token_custom_derivation">カスタム派生パス</string>
<string name="custom_token_custom_derivation_placeholder">m/00\'/0000\'/0\'/0/0</string>
<string name="custom_token_custom_derivation_title">カスタム派生パスを入力</string>
@ -332,15 +337,17 @@
<string name="manage_tokens_unavailable_vote">賛成票を投じる</string>
<string name="manage_tokens_wallet_selector_title">ウォレットを選択</string>
<string name="manage_tokens_wallet_support_only_one_network_title">ウォレットは複数のネットワークをサポートしていません。</string>
<string name="markets_add_to_my_portfolio_description">このアセットの購入、交換、受け取りを開始するには、このトークンを少なくとも1つのネットワークに追加してください。</string>
<string name="markets_add_to_my_portfolio_description">このアセットの購入、交換、受け取りを開始するには、ポートフォリオに追加してください。</string>
<string name="markets_add_to_my_portfolio_unavailable_description">このアセットは利用できません</string>
<string name="markets_add_to_portfolio_button">ポートフォリオに追加</string>
<string name="markets_add_token">トークンを追加</string>
<string name="markets_add_token">追加</string>
<string name="markets_available_networks">利用可能なネットワーク</string>
<string name="markets_common_my_portfolio">私のポートフォリオ</string>
<string name="markets_common_title">マーケット</string>
<string name="markets_generate_addresses_notification">選択したネットワークのアドレスを生成するには、Tangemカードをスキャンする必要があります。</string>
<string name="markets_insights_info_description_message">このセクションのデータは、次のネットワークから取得されています: %s</string>
<string name="markets_loading_error_title">データを読み込めません…</string>
<string name="markets_loading_no_data_title">データなし</string>
<string name="markets_quick_actions">クイックアクション</string>
<string name="markets_search_result_title">結果</string>
<string name="markets_search_see_tokens_under_100k">時価総額10万ドル以下のトークンを見る</string>
@ -365,7 +372,7 @@
<plurals name="markets_token_details_based_on_ratings">
<item quantity="other">%d のレーティングに基づいて</item>
</plurals>
<string name="markets_token_details_blockchain_site">ロックチェーンサイト</string>
<string name="markets_token_details_blockchain_site">ウェブサイト</string>
<string name="markets_token_details_buy_pressure">買い圧力</string>
<string name="markets_token_details_buy_pressure_description">買い手と売り手の取引量の差</string>
<string name="markets_token_details_circulating_supply">循環供給量</string>
@ -446,7 +453,7 @@
</plurals>
<string name="onboarding_seed_generate_title">あなたのシードフレーズ</string>
<plurals name="onboarding_seed_generate_words_count">
<item quantity="other"> %d 単語</item>
<item quantity="other">%d 単語</item>
</plurals>
<string name="onboarding_seed_import_message">ウォレットをインポートするには、下のフィールドにシードフレーズを入力してください。</string>
<string name="onboarding_seed_intro_button_generate">シードフレーズを生成する</string>
@ -497,7 +504,7 @@
<string name="qr_scanner_camera_denied_title">カメラへのアクセスが拒否されました</string>
<string name="receive_bottom_sheet_warning_message">%3$sネットワーク上の%1$s ( %2$s )</string>
<string name="receive_bottom_sheet_warning_message_full">このアドレスには%sのみを送金してください。他のトークンを送信すると、取り返しのつかない損失が発生します。</string>
<string name="receive_token_description">QRコードを表示するか、アドレスを共有します</string>
<string name="receive_token_description">QRコードを表示するか、アドレスを共有してください</string>
<string name="referral_button_participate">参加する</string>
<string name="referral_error_failed_to_load_info">紹介プログラムに関する情報を読み込めませんでした。しばらくしてからもう一度お試しください。</string>
<string name="referral_error_failed_to_load_info_with_reason">紹介プログラムに関する情報を読み込めませんでした。エラー コード: %s 。しばらくしてからもう一度お試しください。</string>
@ -559,18 +566,11 @@
<string name="send_custom_kaspa_per_utxo_footer">Kaspaネットワークで未使用の取引出力UTXOを使用するために必要な手数料です。取引で使用するUTXOが多ければ多いほど、手数料は高くなります。</string>
<string name="send_custom_kaspa_per_utxo_title">UTXOあたりのKAS</string>
<string name="send_date_format">%1$s 、 %2$s</string>
<string name="send_destination_hint_address">アドレス</string>
<string name="send_destination_tag_field">宛先タグ</string>
<string name="send_enter_address_field">アドレスを入力</string>
<string name="send_error_address_same_as_wallet">アドレスはウォレットアドレスと同じです</string>
<string name="send_extras_error_invalid_destination_tag">無効なタグです。取引には追加されません。</string>
<string name="send_extras_error_invalid_memo">無効なメモです。取引には追加されません。</string>
<string name="send_extras_hint_destination_tag">タグ</string>
<string name="send_extras_hint_memo">メモ</string>
<string name="send_fee_include_description">手数料込み</string>
<string name="send_fee_picker_low">低い</string>
<string name="send_fee_picker_normal">普通</string>
<string name="send_fee_picker_priority">優先</string>
<string name="send_fee_unreachable_error_text">ネットワーク接続を確認してください</string>
<string name="send_fee_unreachable_error_title">ネットワーク手数料についての情報にアクセスできません</string>
<string name="send_from_wallet_android">より</string>
@ -585,7 +585,7 @@
<string name="send_network_fee_warning_title">ネットワーク手数料のカバー</string>
<string name="send_notification_exceed_balance_text">手数料と送金額の合計が残高を超えているため、送金に必要な資金が不足しています。</string>
<string name="send_notification_exceed_balance_title">合計が残高を超えています</string>
<string name="send_notification_existential_deposit_text">残高が最低量を下回ると、当アカウントはブロックチェーンから消去されます。残高に%sを残しておいてください</string>
<string name="send_notification_existential_deposit_text">セキュリティリスクを防ぐため、ブロックチェーン上にアカウントを維持するには、少なくとも%s の残高が必要です。この金額は残高に残り、引き出すことはできません</string>
<string name="send_notification_existential_deposit_title">アカウント維持に必要な最低残高</string>
<string name="send_notification_fee_too_high_text">手数料額が推奨額の%s倍となっています。カスタム設定が正しいことを再度確認してください。</string>
<string name="send_notification_fee_too_high_title">カスタム手数料が高くなっています</string>
@ -619,21 +619,15 @@
<string name="send_summary_title">%sを送金する</string>
<string name="send_summary_transaction_description">**%1$s** を送金する (ネットワーク手数料%2$sを含む)</string>
<string name="send_summary_transaction_description_no_fiat_fee">**%1$s** と %2$s を送金しています。</string>
<string name="send_title_currency_format">%sを送信しています</string>
<string name="send_total_label">合計</string>
<string name="send_total_subtitle_asset_format">%1$sと%2$sが送信されます</string>
<string name="send_total_subtitle_fiat_format">≈ %1$s (%2$s: 手数料を含む)</string>
<string name="send_total_subtitle_format">%sが送信されます</string>
<string name="send_transaction_success">取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。</string>
<string name="send_tron_account_activation_error">%1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTronTRXを入金する必要があります。</string>
<string name="send_validation_invalid_address">無効なアドレス</string>
<string name="sent_transaction_sent_title">取引が送信されました</string>
<string name="settings_card_settings_footer">セットアップしたいカードをスキャンするために準備してください。</string>
<string name="settings_forget_wallet">ウォレット削除</string>
<string name="settings_forget_wallet_footer">これにより、ウォレットがアプリから削除されます。ウォレットは再度追加できます。</string>
<string name="settings_wallet_name_title">名前</string>
<string name="staking_active">アクティブ</string>
<string name="staking_active_footer">資産のステーキングを解除するには、ここをクリックしてください。</string>
<string name="staking_active_footer">資産のステーキングを解除するには、上のブロックをタップしてください</string>
<string name="staking_amount_requirement_error">ステーキング金額は %s 以上である必要があります</string>
<string name="staking_claim_unstaked">ステーキング解除分を請求する</string>
<string name="staking_details_annual_percentage_rate">年率</string>
@ -646,25 +640,27 @@
<string name="staking_details_market_rating">市場評価</string>
<string name="staking_details_metrics_block_header">指標</string>
<string name="staking_details_minimum_requirement">最低要件</string>
<string name="staking_details_no_rewards_to_claim">請求できる報酬はありません</string>
<string name="staking_details_no_rewards_to_claim">報酬なし</string>
<string name="staking_details_reward_claiming">請求中の報酬</string>
<string name="staking_details_reward_claiming_info">ステーキング報酬を受け取る方法。自動または手動で請求できます。</string>
<string name="staking_details_reward_claiming_info">ステーキング報酬の受け取り方法。\n自動であなたのアドレスに報酬が入金される方法と、手動で取引を生成して報酬を引き出す方法があります。</string>
<string name="staking_details_reward_schedule">報酬スケジュール</string>
<string name="staking_details_reward_schedule_info">これは、ステーキングの参加者がいつ報酬を受け取るかを決定するスケジュールです。</string>
<string name="staking_details_rewards_to_claim">受け取る報酬: %s</string>
<string name="staking_details_rewards_to_claim">報酬: %s</string>
<string name="staking_details_title">ステーキング%s</string>
<string name="staking_details_unbonding_period">解約完了までの期間</string>
<string name="staking_details_unbonding_period_info">ステーキングから資金の引き出しを要求した後、トークンが利用可能になるまでの待機期間。</string>
<string name="staking_details_warmup_period">ウォームアップ期間</string>
<string name="staking_details_warmup_period_info">ステーキングへの参加を有効にするために割り当てられた時間。</string>
<string name="staking_locked">ロック中</string>
<string name="staking_migrate">移行</string>
<string name="staking_native">ネイティブステーキング</string>
<string name="staking_notification_earn_rewards_text_period_day">ステーキングにより%1$sを獲得できます。ステーキング報酬は毎日受け取れます。</string>
<string name="staking_notification_earn_rewards_text_period_hour">ステーキングにより%1$sを獲得できます。ステーキング報酬は1時間ごとに受け取れます。</string>
<string name="staking_notification_earn_rewards_text_period_hour">ステーキングにより%1$sを獲得できます。ステーキング報酬は毎時間受け取れます。</string>
<string name="staking_notification_earn_rewards_text_period_month">ステーキングにより%1$sを獲得できます。ステーキング報酬は毎月受け取れます。</string>
<string name="staking_notification_earn_rewards_text_period_week">ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。</string>
<string name="staking_notification_earn_rewards_title">ステーキング報酬を獲得</string>
<string name="staking_notification_unstake_text">ステーキング解除後、報酬の獲得はすぐに停止します。ステーキング解除プロセスには%sかかります。</string>
<string name="staking_notification_unstake_text">ステーキング解除プロセスの開始後、報酬の獲得はすぐに停止します。ステーキング解除には%sかかります。</string>
<string name="staking_ready_to_withdraw">引き出し準備完了</string>
<string name="staking_rebond">再結束</string>
<string name="staking_restake">再度ステーキングする</string>
<string name="staking_restake_rewards">報酬をステーキングする</string>
@ -685,8 +681,9 @@
<string name="staking_stake_more">もっとステーキングする</string>
<string name="staking_title_stake">%sをステーキングする</string>
<string name="staking_title_unstake">%sのステーキング解除</string>
<string name="staking_unbonding">ステーキング解約中</string>
<string name="staking_unlocked_locked">ステーキング解除はロックされています</string>
<string name="staking_unstaked">タックされていない</string>
<string name="staking_unstaked">テーキングされていない</string>
<string name="staking_unstaked_footer">資産を請求するために、unstakedを確認してください</string>
<string name="staking_unstaking">ステーキング解除</string>
<string name="staking_validator">バリデーター</string>
@ -822,6 +819,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>
@ -839,7 +838,7 @@
<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_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>
@ -863,8 +862,8 @@
<string name="warning_low_signatures_message">このカードには%sの署名のみが残っています。資金をすべて引き出す必要があります。</string>
<string name="warning_low_signatures_title">署名数が少ないです</string>
<string name="warning_manage_tokens_legacy_derivation_message">異なるネットワーク上のトークンは、異なるアドレスを持つ場合があります。資金を送金する際には、アドレスがネットワークと一致していることを再確認してください。</string>
<string name="warning_matic_migration_title">MATICからPOLへの移行</string>
<string name="warning_matic_migration_message">MATICはPOLに移行中です。ただし、期限は設定されておらず、MATICはまだ廃止されていません。MATICトークンを引き続き安全に使用することも、取引所でPOLに交換することもできます。</string>
<string name="warning_matic_migration_title">MATICからPOLへの移行</string>
<plurals name="warning_missing_derivation_message">
<item quantity="other">%d ネットワークのアドレスを取得するために、カードを利用してください</item>
</plurals>

View file

@ -71,6 +71,7 @@
<string name="common_access_denied">Доступ запрещен</string>
<string name="common_all">Все</string>
<string name="common_allow">Разрешить</string>
<string name="common_analytics">Аналитика</string>
<string name="common_apply">Применить</string>
<string name="common_approval">Одобрение</string>
<string name="common_approve">Подтвердить</string>
@ -107,7 +108,6 @@
<string name="common_explore">Обозреватель</string>
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
<string name="common_explorer">Обозреватель</string>
<string name="common_fee_label">Комиссия</string>
<string name="common_fee_selector_footer">Сетевые комиссии это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s</string>
<string name="common_fee_selector_option_fast">Быстро</string>
<string name="common_fee_selector_option_market">По рынку</string>
@ -117,6 +117,12 @@
<string name="common_go_to_provider">К провайдеру</string>
<string name="common_go_to_token">Перейти в токен</string>
<string name="common_import">Импортировать</string>
<plurals name="common_in_days">
<item quantity="one">через %d день</item>
<item quantity="few">через %d дня</item>
<item quantity="many">через %d дней</item>
<item quantity="other">через %d дней</item>
</plurals>
<string name="common_later">Позже</string>
<string name="common_locked">Заблокирован</string>
<string name="common_main_network">Основная сеть</string>
@ -172,7 +178,7 @@
<string name="custom_token_contract_address_input_title">Адрес контракта</string>
<string name="custom_token_creation_error_invalid_contract_address">Адрес контракта некорректен</string>
<string name="custom_token_creation_error_network_not_selected">Пожалуйста, выберите сеть</string>
<string name="custom_token_creation_error_wrong_decimals">Десятичное число должно быть действительным целым числом, до %li</string>
<string name="custom_token_creation_error_wrong_decimals">Десятичное число должно быть действительным целым числом, до %d</string>
<string name="custom_token_custom_derivation">Своя деривация</string>
<string name="custom_token_custom_derivation_placeholder">Например m/00\'/0000\'/0\'/0/0</string>
<string name="custom_token_custom_derivation_title">Введите свою деривацию</string>
@ -347,6 +353,8 @@
<string name="markets_common_my_portfolio">Мой портфель</string>
<string name="markets_common_title">Рынок</string>
<string name="markets_generate_addresses_notification">Чтобы создать адреса для выбранных сетей, отсканируйте вашу карту Tangem кошелька</string>
<string name="markets_insights_info_description_message">Данные раздела получены из следующих сетей: %s</string>
<string name="markets_loading_no_data_title">Нет данных</string>
<string name="markets_quick_actions">Быстрые действия</string>
<string name="markets_search_result_title">Результат</string>
<string name="markets_search_see_tokens_under_100k">Токены с капитализацией меньше 100к</string>
@ -354,8 +362,8 @@
<string name="markets_search_token_no_result_title">Нет результата</string>
<string name="markets_select_network">Выберите сеть</string>
<string name="markets_select_wallet">Выберите кошелек</string>
<string name="markets_selector_interval_1m_title">ин</string>
<string name="markets_selector_interval_1y_title">1год</string>
<string name="markets_selector_interval_1m_title"></string>
<string name="markets_selector_interval_1y_title">1г</string>
<string name="markets_selector_interval_24h_title">24ч</string>
<string name="markets_selector_interval_3m_title"></string>
<string name="markets_selector_interval_6m_title"></string>
@ -374,7 +382,7 @@
<item quantity="many">На основе %d оценок</item>
<item quantity="other">На основе %d оценок</item>
</plurals>
<string name="markets_token_details_blockchain_site">Сайт блокчейна</string>
<string name="markets_token_details_blockchain_site">Веб-сайт</string>
<string name="markets_token_details_buy_pressure">Покупательское предпочтение</string>
<string name="markets_token_details_buy_pressure_description">Разница между объемом покупателей и продавцов</string>
<string name="markets_token_details_circulating_supply">Циркулирующее предложение</string>
@ -577,18 +585,11 @@
<string name="send_custom_kaspa_per_utxo_footer">Комиссия, которую нужно заплатить за использование каждого неиспользованного выхода транзакции (UTXO) в сети Kaspa. Чем больше UTXO вы используете в транзакции, тем выше будет комиссия.</string>
<string name="send_custom_kaspa_per_utxo_title">KAS за UTXO</string>
<string name="send_date_format">%1$s, %2$s</string>
<string name="send_destination_hint_address">Адрес</string>
<string name="send_destination_tag_field">Код назначения</string>
<string name="send_enter_address_field">Введите адрес</string>
<string name="send_error_address_same_as_wallet">Адрес совпадает с адресом кошелька</string>
<string name="send_extras_error_invalid_destination_tag">Недопустимый Tag. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_error_invalid_memo">Недопустимый Memo. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_fee_include_description">Включая комиссию</string>
<string name="send_fee_picker_low">Низкая</string>
<string name="send_fee_picker_normal">Нормальная</string>
<string name="send_fee_picker_priority">Приоритетная</string>
<string name="send_fee_unreachable_error_text">Проверьте своё интернет соединение</string>
<string name="send_fee_unreachable_error_title">Информация о комиссии сети недоступна</string>
<string name="send_from_wallet_android">Из</string>
@ -603,7 +604,7 @@
<string name="send_network_fee_warning_title">Покрытие сетевой комиссии</string>
<string name="send_notification_exceed_balance_text">Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса</string>
<string name="send_notification_exceed_balance_title">Недостаточно средств</string>
<string name="send_notification_existential_deposit_text">Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, оставьте %s на балансе.</string>
<string name="send_notification_existential_deposit_text">Для сохранения вашего аккаунта в блокчейне и защиты от возможных рисков необходим баланс не менее %s. Эта сумма останется на вашем счете и не может быть снята.</string>
<string name="send_notification_existential_deposit_title">Экзистенциальный депозит</string>
<string name="send_notification_fee_too_high_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
<string name="send_notification_fee_too_high_title">Установлена высокая комиссия</string>
@ -637,24 +638,18 @@
<string name="send_summary_title">Отправка %s</string>
<string name="send_summary_transaction_description">Вы отправляете **%1$s**, включая комиссию сети %2$s</string>
<string name="send_summary_transaction_description_no_fiat_fee">Вы отправляете **%1$s** и %2$s</string>
<string name="send_title_currency_format">Отправка %s</string>
<string name="send_total_label">Всего</string>
<string name="send_total_subtitle_asset_format">%1$s и %2$s будет отправлено</string>
<string name="send_total_subtitle_fiat_format">≈ %1$s (вкл. комиссию: %2$s)</string>
<string name="send_total_subtitle_format">%s будет отправлено</string>
<string name="send_transaction_success">Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время</string>
<string name="send_tron_account_activation_error">%1$s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес.</string>
<string name="send_validation_invalid_address">Неверный адрес</string>
<string name="sent_transaction_sent_title">Транзакция отправлена</string>
<string name="settings_card_settings_footer">Подготовьтесь к сканированию карты, которую вы хотите настроить.</string>
<string name="settings_forget_wallet">Забыть кошелек</string>
<string name="settings_forget_wallet_footer">Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова.</string>
<string name="settings_wallet_name_title">Имя</string>
<string name="staking_active">Активно</string>
<string name="staking_active_footer">Для завершения стейкинга нажмите сюда</string>
<string name="staking_active_footer">Для завершения стейкинга нажмите на блок выше</string>
<string name="staking_amount_requirement_error">Сумма для стейкинга должна быть не менее %s</string>
<string name="staking_claim_unstaked">Забрать средства</string>
<string name="staking_details_annual_percentage_rate">Годовая процентная ставка</string>
<string name="staking_details_annual_percentage_rate">Процентная ставка</string>
<string name="staking_details_annual_percentage_rate_info">Годовой процентный доход, который вы можете получить от участия в стейкинге.</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_available">Доступно</string>
@ -675,6 +670,7 @@
<string name="staking_details_unbonding_period_info">Период, который необходимо подождать после запроса на вывод средств из стейкинга, прежде чем токены станут доступны.</string>
<string name="staking_details_warmup_period">Период прогрева</string>
<string name="staking_details_warmup_period_info">Время, необходимое для начала процесса стейкинга и активации процесса начисления наград</string>
<string name="staking_locked">Заблокировано</string>
<string name="staking_migrate">Переместить</string>
<string name="staking_native">Нативный стейкинг</string>
<string name="staking_notification_earn_rewards_text_period_day">Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый день.</string>
@ -682,12 +678,12 @@
<string name="staking_notification_earn_rewards_text_period_month">Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждый месяц.</string>
<string name="staking_notification_earn_rewards_text_period_week">Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждую неделю.</string>
<string name="staking_notification_earn_rewards_title">Получите награду за стейкинг</string>
<string name="staking_notification_unstake_text">Награда перестанет начисляться сразу после завершения стейкинга. Процесс завершения длится %s.</string>
<string name="staking_restake">Повторный стейкинг</string>
<string name="staking_notification_unstake_text">Награда перестанет начисляться сразу после того, как вы начнете процесс завершения стейкинга. Процесс завершения длится %s.</string>
<string name="staking_restake">Сменить валидатора</string>
<string name="staking_restake_rewards">Застейкать вознаграждения</string>
<string name="staking_revoke">Отозвать</string>
<string name="staking_revote">Переголосовать</string>
<string name="staking_reward_claiming_auto">Автоматически</string>
<string name="staking_reward_claiming_auto">Авто</string>
<string name="staking_reward_claiming_manual">Вручную</string>
<string name="staking_reward_schedule_block">Блок</string>
<string name="staking_reward_schedule_day">День</string>
@ -701,8 +697,9 @@
<string name="staking_stake_locked">Стейкинг закрыт</string>
<string name="staking_stake_more">Застейкать еще</string>
<string name="staking_title_stake">Застейкать %s</string>
<string name="staking_title_unstake">Вывести %s</string>
<string name="staking_unlocked_locked">Разблокировать</string>
<string name="staking_unstaked">Выведено из стейкинга</string>
<string name="staking_unstaked">Вывод из стейкинга</string>
<string name="staking_unstaked_footer">Проверьте процесс завершения стейкинга, чтобы вывести свои средства.</string>
<string name="staking_unstaking">Завершение стейкинга</string>
<string name="staking_validator">Валидатор</string>
@ -882,8 +879,8 @@
<string name="warning_low_signatures_message">На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства.</string>
<string name="warning_low_signatures_title">Малое количество подписей</string>
<string name="warning_manage_tokens_legacy_derivation_message">Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети.</string>
<string name="warning_matic_migration_title">Миграция MATIC в POL</string>
<string name="warning_matic_migration_message">Токен MATIC мигрирует на POL. Однако крайний срок не установлен, и MATIC пока не устарел. Вы можете спокойно продолжать использовать токен MATIC или воспользоваться биржами, чтобы обменять его на POL.</string>
<string name="warning_matic_migration_title">Миграция MATIC в POL</string>
<plurals name="warning_missing_derivation_message">
<item quantity="one">Используйте вашу карту, чтобы получить адрес для %d сети</item>
<item quantity="few">Используйте вашу карту, чтобы получить адреса для %d сетей</item>

View file

@ -78,8 +78,8 @@
<string name="common_attention">Увага</string>
<string name="common_balance">Баланс: %s</string>
<string name="common_balance_title">Баланс</string>
<string name="common_biometric_authentication">біометрична автентифікація</string>
<string name="common_biometrics">біометрією</string>
<string name="common_biometric_authentication">біометричну автентифікацію</string>
<string name="common_biometrics">біометрії</string>
<string name="common_buy">Купити</string>
<string name="common_buy_currency">Перейдіть до %1$s</string>
<string name="common_camera_denied_alert_message">Ви не надали доступ до камери, будь ласка, змініть налаштування конфіденційності</string>
@ -107,7 +107,6 @@
<string name="common_explore">Оглядач</string>
<string name="common_explore_transaction_history">Переглянути історію транзакцій</string>
<string name="common_explorer">Оглядач</string>
<string name="common_fee_label">Комісія</string>
<string name="common_fee_selector_footer">Мережеві комісії це збори, які користувачі сплачують за обробку та підтвердження транзакцій. На розмір комісії може впливати перевантаження мережі, розмір транзакції та пріоритет виконання. %s</string>
<string name="common_fee_selector_option_fast">Швидко</string>
<string name="common_fee_selector_option_market">За ринком</string>
@ -152,7 +151,7 @@
<string name="common_stake">Застейкати</string>
<string name="common_staking">Стейкінг</string>
<string name="common_start">Почати</string>
<string name="common_submit">Надіслати</string>
<string name="common_submit">Продовжити</string>
<string name="common_success">Успіх</string>
<string name="common_support">Підтримка</string>
<string name="common_swap">Обмін</string>
@ -172,7 +171,7 @@
<string name="custom_token_contract_address_input_title">Адреса контракту</string>
<string name="custom_token_creation_error_invalid_contract_address">Адреса контракту недійсна</string>
<string name="custom_token_creation_error_network_not_selected">Будь ласка, оберіть мережу</string>
<string name="custom_token_creation_error_wrong_decimals">Десяткове число повинно бути дійсним цілим числом, до %li</string>
<string name="custom_token_creation_error_wrong_decimals">Десяткове число повинно бути дійсним цілим числом, до %d</string>
<string name="custom_token_custom_derivation">Власна деривація</string>
<string name="custom_token_custom_derivation_placeholder">Наприклад, m/00\'/0000\'/0\'/0/0</string>
<string name="custom_token_custom_derivation_title">Введіть власну деривацію</string>
@ -377,7 +376,7 @@
<item quantity="many">На основі %d оцінок</item>
<item quantity="other">На основі %d оцінок</item>
</plurals>
<string name="markets_token_details_blockchain_site">Блокчейн сайт</string>
<string name="markets_token_details_blockchain_site">Веб-сайт</string>
<string name="markets_token_details_buy_pressure">Давлення покупця</string>
<string name="markets_token_details_buy_pressure_description">Різниця між обсягом покупців та обсягом продавців</string>
<string name="markets_token_details_circulating_supply">Циркуляційний запас</string>
@ -473,7 +472,7 @@
<string name="onboarding_seed_intro_title">Використовувати seed-фразу</string>
<string name="onboarding_seed_mnemonic_invalid_checksum">Невірна seed-фраза. Будь ласка, перевірте порядок слів.</string>
<string name="onboarding_seed_mnemonic_wrong_words">Невірна seed-фраза. Будь ласка, перевірте правопис.</string>
<string name="onboarding_seed_phrase_intro_legacy">Застарілий</string>
<string name="onboarding_seed_phrase_intro_legacy">Застаріло</string>
<string name="onboarding_seed_user_validation_message">Щоб перевірити чи правильно ви записали seed-фразу, введіть 2-е, 7-е та 11-те слова</string>
<string name="onboarding_seed_user_validation_title">Отже, давайте перевіримо</string>
<string name="onboarding_subtitle_no_backup_cards">Щоб почати процес резервного копіювання, додайте одну або дві резервні картки.</string>
@ -530,7 +529,7 @@
<item quantity="many">за %d гаманців</item>
<item quantity="other">за %d гаманців</item>
</plurals>
<string name="referral_point_currencies_description">Отримайте ^^%1$s^^ на вашу адресу в мережі %2$s%3$s ^^через 30 днів^^ за кожен гаманець, який придбає ваш друг</string>
<string name="referral_point_currencies_description">Отримаєте ^^%1$s^^ на вашу адресу в мережі %2$s %3$s ^^через 30 днів^^ за кожен гаманець, який придбає ваш друг</string>
<string name="referral_point_currencies_title">Ви</string>
<string name="referral_point_discount_description_prefix">Отримає</string>
<string name="referral_point_discount_description_suffix">при купівлі гаманця на сайті tangem.com</string>
@ -583,18 +582,11 @@
<string name="send_custom_kaspa_per_utxo_footer">Комісія, необхідна за використання кожної невитраченої транзакції (UTXO) у мережі Kaspa. Чим більше UTXO ви використовуєте в транзакції, тим вищою буде комісія.</string>
<string name="send_custom_kaspa_per_utxo_title">KAS за UTXO</string>
<string name="send_date_format">%1$s, %2$s</string>
<string name="send_destination_hint_address">Адреса</string>
<string name="send_destination_tag_field">Тег призначення</string>
<string name="send_enter_address_field">Введіть адресу</string>
<string name="send_error_address_same_as_wallet">Адреса збігається з адресою гаманця</string>
<string name="send_extras_error_invalid_destination_tag">Недопустимий Tag. Він не буде доданий у транзакцію.</string>
<string name="send_extras_error_invalid_memo">Недопустимий Memo. Він не буде доданий до транзакції.</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_fee_include_description">Включаючи комісію</string>
<string name="send_fee_picker_low">Низька</string>
<string name="send_fee_picker_normal">Нормальна</string>
<string name="send_fee_picker_priority">Пріоритетна</string>
<string name="send_fee_unreachable_error_text">Перевірте підключення до мережі</string>
<string name="send_fee_unreachable_error_title">Інформація щодо комісії в мережі недоступна</string>
<string name="send_from_wallet_android">Із</string>
@ -609,7 +601,7 @@
<string name="send_network_fee_warning_title">Покриття мережевої комісії</string>
<string name="send_notification_exceed_balance_text">Недостатньо коштів для здійснення переказу, оскільки загальна сума комісії та переказу перевищує наявний баланс</string>
<string name="send_notification_exceed_balance_title">Сума перевищує баланс</string>
<string name="send_notification_existential_deposit_text">Рахунок буде видалено з блокчейну, якщо баланс стане нижчим за екзистенційний депозит. Будь ласка, залиште %s на своєму балансі.</string>
<string name="send_notification_existential_deposit_text">Для збереження вашого акаунту у блокчейні та захисту від можливих ризиків необхідний баланс не менше %s. Ця сума залишиться на вашому рахунку та не може бути знята.</string>
<string name="send_notification_existential_deposit_title">Екзистенційний депозит</string>
<string name="send_notification_fee_too_high_text">Сума комісії в %s разів перевищує рекомендовану. Переконайтеся, що користувацькі налаштування вірні.</string>
<string name="send_notification_fee_too_high_title">Встановлена комісія завелика</string>
@ -643,23 +635,18 @@
<string name="send_summary_title">Надіслати %s</string>
<string name="send_summary_transaction_description">Ви надсилаєте **%1$s**, включно з комісію мережі %2$s</string>
<string name="send_summary_transaction_description_no_fiat_fee">Ви надсилаєте **%1$s** і %2$s</string>
<string name="send_title_currency_format">Надсилання %s</string>
<string name="send_total_label">Всього</string>
<string name="send_total_subtitle_asset_format">%1$s та %2$s буде надіслано</string>
<string name="send_total_subtitle_fiat_format">≈ %1$s (вкл. комісію: %2$s )</string>
<string name="send_total_subtitle_format">%s буде надіслано</string>
<string name="send_transaction_success">Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час</string>
<string name="send_tron_account_activation_error">%1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron (TRX) на свій рахунок.</string>
<string name="send_validation_invalid_address">Недійсна адреса</string>
<string name="sent_transaction_sent_title">Трансакцію надіслано</string>
<string name="settings_card_settings_footer">Підготуйтеся до сканування картку, яку потрібно налаштувати.</string>
<string name="settings_card_settings_footer">Підготуйте до сканування картку, яку потрібно налаштувати.</string>
<string name="settings_forget_wallet">Забути гаманець</string>
<string name="settings_forget_wallet_footer">Це призведе до видалення гаманця з застосунку. Сам гаманець можна додати знову.</string>
<string name="settings_wallet_name_title">Ім\'я</string>
<string name="staking_active">Активний</string>
<string name="staking_active_footer">Щоб вивести активи зі стейкінгу, натисніть тут.</string>
<string name="staking_active_footer">Для завершення стейкінгу натисніть на блок вище</string>
<string name="staking_amount_requirement_error">Сума для стейкінгу має бути не менше %s</string>
<string name="staking_claim_unstaked">Зняти кошти</string>
<string name="staking_details_annual_percentage_rate">Процентна ставка</string>
<string name="staking_details_annual_percentage_rate_info">Річний відсоток, який ви можете отримати, беручи участь у стейкінгу.</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_available">Доступно</string>
@ -703,7 +690,7 @@
<string name="staking_reward_schedule_month">Місяць</string>
<string name="staking_reward_schedule_week">Тиждень</string>
<string name="staking_rewards">Винагороди</string>
<string name="staking_stake_locked">Застейкати</string>
<string name="staking_stake_locked">Стейкінг закрито</string>
<string name="staking_stake_more">Застейкати більше</string>
<string name="staking_title_stake">Застейкати %s</string>
<string name="staking_title_unstake">Зняти зі стейкінгу %s</string>

View file

@ -57,12 +57,12 @@
<string name="common_enable">允許</string>
<string name="common_enabled">啟用</string>
<string name="common_error">錯誤</string>
<string name="common_fee_label">費用</string>
<string name="common_import">導入</string>
<string name="common_network_fee_title">網路費</string>
<string name="common_no"></string>
<string name="common_ok">OK</string>
<string name="common_origin_card">主卡片</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_reject">拒絕</string>
<string name="common_rename">重新命名</string>
<string name="common_save_changes">保存設置</string>
@ -250,24 +250,11 @@
<string name="scan_card_settings_message">掃描卡片以更改其設置。這些更改只會影響您掃描過的卡,不會影響綁定到您錢包的其他卡。</string>
<string name="scan_card_settings_title">準備好您的卡!</string>
<string name="send_amount_label">數量</string>
<string name="send_destination_hint_address">地址</string>
<string name="send_error_address_same_as_wallet">地址與錢包地址相同</string>
<string name="send_extras_error_invalid_destination_tag">標籤無效。它不會被添加到交易中</string>
<string name="send_extras_error_invalid_memo">Memo無效。 它不會被添加到交易中</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_fee_include_description">包含費用</string>
<string name="send_fee_picker_low"></string>
<string name="send_fee_picker_normal">正常</string>
<string name="send_fee_picker_priority">優先</string>
<string name="send_max_amount_label">最大值</string>
<string name="send_title_currency_format">發送 %s</string>
<string name="send_total_label">總計</string>
<string name="send_total_subtitle_asset_format">%1$s 和 %2$s 將被發送</string>
<string name="send_total_subtitle_fiat_format">≈ %1$s (費用包含: %2$s)</string>
<string name="send_total_subtitle_format">%s 將被發送</string>
<string name="send_transaction_success">交易已成功簽署並發送至區塊鏈節點。錢包餘額稍後更新</string>
<string name="send_validation_invalid_address">無效地址</string>
<string name="story_awe_description">安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中</string>
<string name="story_awe_title">創新式的硬體錢包</string>
<string name="story_backup_description">最多 3張實體卡片 到一個錢包</string>

View file

@ -70,6 +70,7 @@
<string name="common_access_denied">Access denied</string>
<string name="common_all">All</string>
<string name="common_allow">Allow</string>
<string name="common_analytics">Analytics</string>
<string name="common_apply">Apply</string>
<string name="common_approval">Approval</string>
<string name="common_approve">Approve</string>
@ -82,6 +83,7 @@
<string name="common_buy_currency">Go to %1$s</string>
<string name="common_camera_denied_alert_message">You have not given access to your camera, please adjust your privacy settings</string>
<string name="common_cancel">Cancel</string>
<string name="common_choose_action">Choose action</string>
<string name="common_claim_rewards">Claim rewards</string>
<string name="common_close">Close</string>
<string name="common_continue">Continue</string>
@ -104,7 +106,6 @@
<string name="common_explore">Explore</string>
<string name="common_explore_transaction_history">Explore transaction history</string>
<string name="common_explorer">Explorer</string>
<string name="common_fee_label">Fee</string>
<string name="common_fee_selector_footer">Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s</string>
<string name="common_fee_selector_option_fast">Fast</string>
<string name="common_fee_selector_option_market">Market</string>
@ -114,6 +115,10 @@
<string name="common_go_to_provider">Go to provider</string>
<string name="common_go_to_token">Go to token</string>
<string name="common_import">Import</string>
<plurals name="common_in_days">
<item quantity="one">in %d day</item>
<item quantity="other">in %d days</item>
</plurals>
<string name="common_later">Later</string>
<string name="common_locked">Locked</string>
<string name="common_main_network">Main network</string>
@ -169,7 +174,9 @@
<string name="custom_token_contract_address_input_title">Contract address</string>
<string name="custom_token_creation_error_invalid_contract_address">Contract address is invalid</string>
<string name="custom_token_creation_error_network_not_selected">Please select the network</string>
<string name="custom_token_creation_error_wrong_decimals">Decimal must be a valid integer, up to %li</string>
<string name="custom_token_creation_error_token_already_exist_message">This token has already been added to the list</string>
<string name="custom_token_creation_error_token_already_exist_title">Token already exists</string>
<string name="custom_token_creation_error_wrong_decimals">Decimal must be a valid integer, up to %d</string>
<string name="custom_token_custom_derivation">Custom derivation</string>
<string name="custom_token_custom_derivation_placeholder">E. g. m/00\'/0000\'/0\'/0/0</string>
<string name="custom_token_custom_derivation_title">Enter custom derivation</string>
@ -344,7 +351,9 @@
<string name="markets_common_my_portfolio">My portfolio</string>
<string name="markets_common_title">Market</string>
<string name="markets_generate_addresses_notification">To generate addresses for selected networks, you must scan your Tangem Wallet card</string>
<string name="markets_insights_info_description_message">This sections data is sourced from the following networks: %s</string>
<string name="markets_loading_error_title">Unable to load the data…</string>
<string name="markets_loading_no_data_title">No data</string>
<string name="markets_quick_actions">Quick actions</string>
<string name="markets_search_result_title">Result</string>
<string name="markets_search_see_tokens_under_100k">See tokens under 100k market cap</string>
@ -370,7 +379,7 @@
<item quantity="one">Based on %d rating</item>
<item quantity="other">Based on %d ratings</item>
</plurals>
<string name="markets_token_details_blockchain_site">Blockchain site</string>
<string name="markets_token_details_blockchain_site">Website</string>
<string name="markets_token_details_buy_pressure">Buy pressure</string>
<string name="markets_token_details_buy_pressure_description">The difference between buyers volume and sellers volume</string>
<string name="markets_token_details_circulating_supply">Circulating supply</string>
@ -568,18 +577,11 @@
<string name="send_custom_kaspa_per_utxo_footer">The fee required for using each unspent transaction output (UTXO) in the Kaspa network. The more UTXOs you use in a transaction, the higher the fee will be.</string>
<string name="send_custom_kaspa_per_utxo_title">KAS per UTXO</string>
<string name="send_date_format">%1$s, %2$s</string>
<string name="send_destination_hint_address">Address</string>
<string name="send_destination_tag_field">Destination Tag</string>
<string name="send_enter_address_field">Enter address</string>
<string name="send_error_address_same_as_wallet">Address is the same as wallet address</string>
<string name="send_extras_error_invalid_destination_tag">Invalid Tag. It won\'t be added to the transaction.</string>
<string name="send_extras_error_invalid_memo">Invalid Memo. It won\'t be added to the transaction.</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_fee_include_description">Include fee</string>
<string name="send_fee_picker_low">Low</string>
<string name="send_fee_picker_normal">Normal</string>
<string name="send_fee_picker_priority">Priority</string>
<string name="send_fee_unreachable_error_text">Check your network connection</string>
<string name="send_fee_unreachable_error_title">Network fee info unreachable</string>
<string name="send_from_wallet_android">From</string>
@ -594,7 +596,7 @@
<string name="send_network_fee_warning_title">Network fee coverage</string>
<string name="send_notification_exceed_balance_text">Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance</string>
<string name="send_notification_exceed_balance_title">Total exceeds balance</string>
<string name="send_notification_existential_deposit_text">The account will be wiped from the blockchain if a balance goes below the existential deposit. Please leave %s on your balance.</string>
<string name="send_notification_existential_deposit_text">A balance of at least %s is required to keep your account on the blockchain to prevent security risks. This amount will remain in your balance and cannot be withdrawn.</string>
<string name="send_notification_existential_deposit_title">Existential deposit</string>
<string name="send_notification_fee_too_high_text">The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.</string>
<string name="send_notification_fee_too_high_title">Custom fee is high</string>
@ -628,21 +630,15 @@
<string name="send_summary_title">Send %s</string>
<string name="send_summary_transaction_description">You are sending **%1$s** including a network fee of %2$s</string>
<string name="send_summary_transaction_description_no_fiat_fee">You are sending **%1$s** and %2$s</string>
<string name="send_title_currency_format">Sending %s</string>
<string name="send_total_label">Total</string>
<string name="send_total_subtitle_asset_format">%1$s and %2$s will be sent</string>
<string name="send_total_subtitle_fiat_format">≈ %1$s (inc. fee: %2$s)</string>
<string name="send_total_subtitle_format">%s will be sent</string>
<string name="send_transaction_success">Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while</string>
<string name="send_tron_account_activation_error">%1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron (TRX) in your account.</string>
<string name="send_validation_invalid_address">Invalid address</string>
<string name="sent_transaction_sent_title">Transaction sent</string>
<string name="settings_card_settings_footer">Prepare to scan card you want to setup.</string>
<string name="settings_forget_wallet">Forget wallet</string>
<string name="settings_forget_wallet_footer">This will remove the wallet from the application. The wallet itself can be added again.</string>
<string name="settings_wallet_name_title">Name</string>
<string name="staking_active">Active</string>
<string name="staking_active_footer">To unstake your assets, click here.</string>
<string name="staking_active_footer">To unstake your assets, tap the block above</string>
<string name="staking_amount_requirement_error">The amount to stake must be at least %s</string>
<string name="staking_claim_unstaked">Claim unstaked</string>
<string name="staking_details_annual_percentage_rate">Annual percentage rate</string>
@ -655,25 +651,28 @@
<string name="staking_details_market_rating">Market rating</string>
<string name="staking_details_metrics_block_header">Metrics</string>
<string name="staking_details_minimum_requirement">Minimum Requirement</string>
<string name="staking_details_no_rewards_to_claim">No rewards to claim</string>
<string name="staking_details_no_rewards_to_claim">No rewards</string>
<string name="staking_details_reward_claiming">Reward claiming</string>
<string name="staking_details_reward_claiming_info">Method of receiving staking rewards.\nIt can be either automatic, where the reward is credited to your address, or manual, where you need to withdraw the reward by creating a transaction to receive it.</string>
<string name="staking_details_reward_schedule">Reward schedule</string>
<string name="staking_details_reward_schedule_info">This is a schedule that determines when participants in staking receive their rewards.</string>
<string name="staking_details_rewards_to_claim">Rewards to claim: %s</string>
<string name="staking_details_rewards_to_claim">Rewards: %s</string>
<string name="staking_details_title">Staking %s</string>
<string name="staking_details_unbonding_period">Unbonding Period</string>
<string name="staking_details_unbonding_period_info">The period you must wait after requesting to withdraw funds from staking before the tokens become available.</string>
<string name="staking_details_warmup_period">Warmup period</string>
<string name="staking_details_warmup_period_info">The allocated time for activating participation in staking.</string>
<string name="staking_locked">Locked</string>
<string name="staking_migrate">Migrate</string>
<string name="staking_native">Native staking</string>
<string name="staking_notification_earn_rewards_text_period_day">Staking allow you to earn %1$s. Your staking rewards arrive every day.</string>
<string name="staking_notification_earn_rewards_text_period_hour">Staking allow you to earn %1$s. Your staking rewards arrive every hour.</string>
<string name="staking_notification_earn_rewards_text_period_month">Staking allow you to earn %1$s. Your staking rewards arrive every month.</string>
<string name="staking_notification_earn_rewards_text_period_week">Staking allow you to earn %1$s. Your staking rewards arrive every week.</string>
<string name="staking_notification_earn_rewards_text_period_day">Staking allows you to earn %1$s. Your staking rewards arrive every day.</string>
<string name="staking_notification_earn_rewards_text_period_hour">Staking allows you to earn %1$s. Your staking rewards arrive every hour.</string>
<string name="staking_notification_earn_rewards_text_period_month">Staking allows you to earn %1$s. Your staking rewards arrive every month.</string>
<string name="staking_notification_earn_rewards_text_period_week">Staking allows you to earn %1$s. Your staking rewards arrive every week.</string>
<string name="staking_notification_earn_rewards_title">Earn staking rewards</string>
<string name="staking_notification_unstake_text">Rewards stop accruing immediately after you unstake. The unstaking process takes %s.</string>
<string name="staking_notification_unstake_text">Rewards stop accruing immediately after you start unstaking. The unstaking process takes %s.</string>
<string name="staking_preparing">Preparing</string>
<string name="staking_ready_to_withdraw">Ready to withdraw</string>
<string name="staking_rebond">Rebond</string>
<string name="staking_restake">Restake</string>
<string name="staking_restake_rewards">Restake rewards</string>
@ -694,6 +693,7 @@
<string name="staking_stake_more">Stake more</string>
<string name="staking_title_stake">Stake %s</string>
<string name="staking_title_unstake">Unstake %s</string>
<string name="staking_unbonding">Unbonding</string>
<string name="staking_unlocked_locked">Unlock locked</string>
<string name="staking_unstaked">Unstaked</string>
<string name="staking_unstaked_footer">Check unstaked to claim your assets</string>
@ -875,8 +875,8 @@
<string name="warning_low_signatures_message">Only %s signatures are left on this card. You must withdraw all of your funds.</string>
<string name="warning_low_signatures_title">Low signature count</string>
<string name="warning_manage_tokens_legacy_derivation_message">Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds.</string>
<string name="warning_matic_migration_title">MATIC to POL Migration</string>
<string name="warning_matic_migration_message">MATIC is being migrated to POL. However there is no deadline set and MATIC isn\'t being deprecated yet. You can safely continue using MATIC token or use exchanges to swap it for POL.</string>
<string name="warning_matic_migration_title">MATIC to POL Migration</string>
<plurals name="warning_missing_derivation_message">
<item quantity="one">Use your card to get an address for %d network</item>
<item quantity="other">Use your card to get an addresses for %d networks</item>

View file

@ -40,6 +40,7 @@ dependencies {
implementation(deps.compose.coil)
implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt)
implementation(deps.compose.reorderable)
/** Other libraries */
implementation(deps.compose.accompanist.systemUiController)

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

@ -49,10 +49,10 @@ import kotlinx.collections.immutable.toImmutableList
@Composable
fun BasicDialog(
message: String,
confirmButton: DialogButton,
confirmButton: DialogButtonUM,
onDismissDialog: () -> Unit,
title: String? = null,
dismissButton: DialogButton? = null,
dismissButton: DialogButtonUM? = null,
isDismissable: Boolean = true,
) {
TangemDialog(
@ -72,7 +72,7 @@ fun BasicDialog(
fun SimpleOkDialog(message: String, onDismissDialog: () -> Unit) {
TangemDialog(
type = DialogType.Message(message),
confirmButton = DialogButton(onClick = onDismissDialog),
confirmButton = DialogButtonUM(onClick = onDismissDialog),
onDismissDialog = onDismissDialog,
title = null,
dismissButton = null,
@ -97,12 +97,12 @@ fun SimpleOkDialog(message: String, onDismissDialog: () -> Unit) {
@Composable
fun TextInputDialog(
fieldValue: TextFieldValue,
confirmButton: DialogButton,
confirmButton: DialogButtonUM,
onDismissDialog: () -> Unit,
onValueChange: (TextFieldValue) -> Unit,
textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() },
textFieldParams: AdditionalTextInputDialogUM = remember { AdditionalTextInputDialogUM() },
title: String? = null,
dismissButton: DialogButton? = null,
dismissButton: DialogButtonUM? = null,
isDismissable: Boolean = true,
) {
TangemDialog(
@ -125,12 +125,12 @@ fun TextInputDialog(
@Composable
fun TextInputDialog(
fieldValue: String,
confirmButton: DialogButton,
confirmButton: DialogButtonUM,
onDismissDialog: () -> Unit,
onValueChange: (String) -> Unit,
textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() },
textFieldParams: AdditionalTextInputDialogUM = remember { AdditionalTextInputDialogUM() },
title: String? = null,
dismissButton: DialogButton? = null,
dismissButton: DialogButtonUM? = null,
isDismissable: Boolean = true,
) {
TangemDialog(
@ -154,7 +154,7 @@ fun TextInputDialog(
fun SelectorDialog(
selectedItemIndex: Int,
items: ImmutableList<String>,
confirmButton: DialogButton,
confirmButton: DialogButtonUM,
onSelect: (index: Int) -> Unit,
onDismissDialog: () -> Unit,
title: String? = null,
@ -180,7 +180,7 @@ fun SelectorDialog(
* @param enabled If false button will be disabled
* @param onClick Button click callback
*/
data class DialogButton(
data class DialogButtonUM(
val title: String? = null,
val warning: Boolean = false,
val enabled: Boolean = true,
@ -190,7 +190,7 @@ data class DialogButton(
/**
* Additional params for dialog text field
*/
data class AdditionalTextInputDialogParams(
data class AdditionalTextInputDialogUM(
val label: String? = null,
val placeholder: String? = null,
val caption: String? = null,
@ -203,10 +203,10 @@ data class AdditionalTextInputDialogParams(
@Composable
private fun TangemDialog(
type: DialogType,
confirmButton: DialogButton,
confirmButton: DialogButtonUM,
onDismissDialog: () -> Unit,
title: String? = null,
dismissButton: DialogButton? = null,
dismissButton: DialogButtonUM? = null,
properties: DialogProperties = DialogProperties(),
) {
Dialog(properties = properties, onDismissRequest = onDismissDialog) {
@ -304,7 +304,11 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
}
@Composable
private fun DialogButtons(confirmButton: DialogButton, dismissButton: DialogButton?, modifier: Modifier = Modifier) {
private fun DialogButtons(
confirmButton: DialogButtonUM,
dismissButton: DialogButtonUM?,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(
@ -413,13 +417,13 @@ private sealed class DialogType {
data class TextInput(
val value: TextFieldValue,
val onValueChange: (TextFieldValue) -> Unit,
val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(),
val params: AdditionalTextInputDialogUM = AdditionalTextInputDialogUM(),
) : DialogType()
data class SimpleTextInput(
val value: String,
val onValueChange: (String) -> Unit,
val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(),
val params: AdditionalTextInputDialogUM = AdditionalTextInputDialogUM(),
) : DialogType()
data class Selector(
@ -445,8 +449,8 @@ private fun BasicDialogPreview() {
message = "All protected passwords will be deleted from the secure storage, you must enter the wallet " +
"password to work with the app",
title = "Attention",
confirmButton = DialogButton {},
dismissButton = DialogButton {},
confirmButton = DialogButtonUM {},
dismissButton = DialogButtonUM {},
onDismissDialog = {},
)
}
@ -478,8 +482,8 @@ private fun WarningBasicDialogSample(modifier: Modifier = Modifier) {
message = "All protected passwords will be deleted from the secure storage, you must enter the wallet " +
"password to work with the app",
title = "Attention",
confirmButton = DialogButton(warning = true) {},
dismissButton = DialogButton {},
confirmButton = DialogButtonUM(warning = true) {},
dismissButton = DialogButtonUM {},
onDismissDialog = {},
)
}
@ -502,10 +506,10 @@ private fun TextInputDialogSample(modifier: Modifier = Modifier) {
TextInputDialog(
fieldValue = TextFieldValue(text = ""),
title = "Rename Wallet",
confirmButton = DialogButton {},
confirmButton = DialogButtonUM {},
onDismissDialog = {},
onValueChange = {},
textFieldParams = AdditionalTextInputDialogParams(
textFieldParams = AdditionalTextInputDialogUM(
label = "Wallet name",
),
)
@ -530,7 +534,7 @@ private fun SelectorDialogPreview(@PreviewParameter(SelctorDialogParamsProvider:
title = param.title,
items = param.items,
selectedItemIndex = param.selectedItemIndex,
confirmButton = DialogButton(title = "Cancel", onClick = {}),
confirmButton = DialogButtonUM(title = "Cancel", onClick = {}),
onSelect = {},
onDismissDialog = {},
)

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

@ -9,7 +9,9 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape
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
@ -25,28 +27,44 @@ import kotlinx.collections.immutable.persistentListOf
@Immutable
class InformationBlockContentScope(val scope: BoxScope) : BoxScope by scope
// TODO: [REDACTED_JIRA]
@Composable
fun InformationBlock(
title: @Composable BoxScope.() -> Unit,
modifier: Modifier = Modifier,
contentHorizontalPadding: Dp = TangemTheme.dimens.spacing12,
shape: Shape = TangemTheme.shapes.roundedCornersXMedium,
action: (@Composable BoxScope.() -> Unit)? = null,
content: (@Composable InformationBlockContentScope.() -> Unit)? = null,
) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.clip(shape)
.background(color = TangemTheme.colors.background.action),
horizontalAlignment = Alignment.Start,
) {
val minHeight = if (action == null) TangemTheme.dimens.size36 else TangemTheme.dimens.size40
val padding = if (action == null) {
PaddingValues(
start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing4,
)
} else {
PaddingValues(
start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing11,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing5,
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size40)
.padding(
top = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing6,
)
.padding(horizontal = TangemTheme.dimens.spacing12),
.heightIn(min = minHeight)
.padding(paddingValues = padding),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@ -72,7 +90,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

@ -11,4 +11,9 @@ data class TangemBottomSheetConfig(
val isShow: Boolean,
val onDismissRequest: () -> Unit,
val content: TangemBottomSheetConfigContent,
)
) {
companion object {
val Empty = TangemBottomSheetConfig(false, {}, TangemBottomSheetConfigContent.Empty)
}
}

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

@ -1,5 +1,10 @@
package com.tangem.core.ui.components.buttons.common
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
@ -36,6 +41,7 @@ fun TangemButton(
textStyle: TextStyle = TangemTheme.typography.button,
shape: Shape = size.toShape(),
iconPadding: Dp = size.toIconPadding(),
animateContentChange: Boolean = false,
) {
val multipleClickPreventer = remember { MultipleClickPreventer.get() }
@ -56,6 +62,7 @@ fun TangemButton(
buttonIcon = icon,
iconPadding = iconPadding,
showProgress = showProgress,
animateContentChange = animateContentChange,
progressIndicator = {
CircularProgressIndicator(
modifier = Modifier.buttonContentSize(maxContentSize),
@ -108,24 +115,54 @@ private inline fun RowScope.ButtonContentContainer(
buttonIcon: TangemButtonIconPosition,
iconPadding: Dp,
showProgress: Boolean,
animateContentChange: Boolean,
progressIndicator: @Composable RowScope.() -> Unit,
text: @Composable RowScope.() -> Unit,
icon: @Composable RowScope.(Int) -> Unit,
crossinline text: @Composable RowScope.() -> Unit,
crossinline icon: @Composable RowScope.(Int) -> Unit,
additionalText: @Composable () -> Unit,
) {
if (showProgress) {
progressIndicator()
} else {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row(horizontalArrangement = Arrangement.Center) {
if (buttonIcon is TangemButtonIconPosition.Start) {
icon(buttonIcon.iconResId)
Spacer(modifier = Modifier.requiredWidth(iconPadding))
if (animateContentChange) {
AnimatedContent(
targetState = buttonIcon,
transitionSpec = {
fadeIn(tween(durationMillis = 220)) togetherWith
fadeOut(tween(durationMillis = 220))
},
label = "button text with icon",
) { iconState ->
Row(horizontalArrangement = Arrangement.Center) {
when (iconState) {
is TangemButtonIconPosition.Start -> {
icon(iconState.iconResId)
Spacer(modifier = Modifier.requiredWidth(iconPadding))
text()
}
is TangemButtonIconPosition.End -> {
text()
Spacer(modifier = Modifier.requiredWidth(iconPadding))
icon(iconState.iconResId)
}
is TangemButtonIconPosition.None -> {
text()
}
}
}
}
text()
if (buttonIcon is TangemButtonIconPosition.End) {
Spacer(modifier = Modifier.requiredWidth(iconPadding))
icon(buttonIcon.iconResId)
} else {
Row(horizontalArrangement = Arrangement.Center) {
if (buttonIcon is TangemButtonIconPosition.Start) {
icon(buttonIcon.iconResId)
Spacer(modifier = Modifier.requiredWidth(iconPadding))
}
text()
if (buttonIcon is TangemButtonIconPosition.End) {
Spacer(modifier = Modifier.requiredWidth(iconPadding))
icon(buttonIcon.iconResId)
}
}
}
additionalText()

View file

@ -38,7 +38,7 @@ sealed class CurrencyIconState {
* Represents a token icon.
*
* @property url The URL where the token icon can be fetched from. May be `null` if not found.
* @property topBadgeIconResId The drawable resource ID for the network badge.
* @property topBadgeIconResId The drawable resource ID for the network badge. May be `null`.
* @property isGrayscale Specifies whether to show the icon in grayscale.
* @property showCustomBadge Specifies whether to show the custom token badge.
* @property fallbackTint The color to be used for tinting the fallback icon.
@ -46,7 +46,7 @@ sealed class CurrencyIconState {
*/
data class TokenIcon(
val url: String?,
@DrawableRes override val topBadgeIconResId: Int,
@DrawableRes override val topBadgeIconResId: Int?,
override val isGrayscale: Boolean,
override val showCustomBadge: Boolean,
val fallbackTint: Color,
@ -81,4 +81,28 @@ sealed class CurrencyIconState {
override val showCustomBadge: Boolean = false
override val topBadgeIconResId: Int? = null
}
fun copySealed(
isGrayscale: Boolean = this.isGrayscale,
showCustomBadge: Boolean = this.showCustomBadge,
topBadgeIconResId: Int? = this.topBadgeIconResId,
): CurrencyIconState = when (this) {
is CoinIcon -> copy(
isGrayscale = isGrayscale,
showCustomBadge = showCustomBadge,
)
is CustomTokenIcon -> copy(
isGrayscale = isGrayscale,
showCustomBadge = showCustomBadge,
topBadgeIconResId = topBadgeIconResId ?: this.topBadgeIconResId,
)
is TokenIcon -> copy(
isGrayscale = isGrayscale,
showCustomBadge = showCustomBadge,
topBadgeIconResId = topBadgeIconResId,
)
is Loading,
is Locked,
-> this
}
}

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
@ -69,6 +70,7 @@ fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFie
textStyle = TangemTheme.typography.body2.copy(
color = TangemTheme.colors.text.primary1,
),
cursorBrush = SolidColor(TangemTheme.colors.icon.primary1),
decorationBox = @Composable { innerTextField ->
DecorationBox(
state = state,

View file

@ -43,9 +43,9 @@ import com.tangem.core.ui.res.TangemThemePreview
*/
@Composable
fun InputRowDefault(
title: TextReference,
text: TextReference,
modifier: Modifier = Modifier,
title: TextReference? = null,
titleColor: Color = TangemTheme.colors.text.secondary,
textColor: Color = TangemTheme.colors.text.primary1,
iconRes: Int? = null,
@ -66,16 +66,18 @@ fun InputRowDefault(
modifier = Modifier
.weight(1f),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
title?.let {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = titleColor,
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing8),
)
}
Text(
text = text.resolveReference(),
style = TangemTheme.typography.body2,
color = textColor,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
iconRes?.let {

View file

@ -0,0 +1,26 @@
package com.tangem.core.ui.components.list
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.*
@Composable
fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buffer: Int = 2) {
val loadMore by remember {
derivedStateOf {
val layoutInfo = listState.layoutInfo
val totalItemsNumber = layoutInfo.totalItemsCount
val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1
lastVisibleItemIndex > totalItemsNumber - buffer
}
}
val totalItemsCount by remember { derivedStateOf { listState.layoutInfo.totalItemsCount } }
var emitted by remember(totalItemsCount) { mutableStateOf(false) }
LaunchedEffect(loadMore) {
if (loadMore && !emitted) {
emitted = onLoadMore()
}
}
}

View file

@ -1,6 +1,21 @@
package com.tangem.core.ui.components.marketprice
import java.math.BigDecimal
import java.math.RoundingMode
/** Price changing type */
enum class PriceChangeType {
UP, DOWN, NEUTRAL,
;
companion object {
@Suppress("MagicNumber")
fun fromBigDecimal(priceChangePercent: BigDecimal): PriceChangeType {
return when {
priceChangePercent < BigDecimal.ZERO -> DOWN
priceChangePercent.setScale(4, RoundingMode.HALF_UP) > BigDecimal.ZERO -> UP
else -> NEUTRAL
}
}
}
}

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)
},
)
},
@ -136,6 +137,7 @@ private fun Preview_BlockchainRow(@PreviewParameter(BlockchainRowParameterProvid
private class BlockchainRowParameterProvider : CollectionPreviewParameterProvider<BlockchainRowUM>(
collection = listOf(
BlockchainRowUM(
id = "0",
name = "BNB BEACON CHAIN",
type = "BEP20",
iconResId = R.drawable.img_bsc_22,
@ -143,6 +145,7 @@ private class BlockchainRowParameterProvider : CollectionPreviewParameterProvide
isSelected = true,
),
BlockchainRowUM(
id = "1",
name = "1234567890111213141516171819",
type = "BEP20",
iconResId = R.drawable.ic_bsc_16,
@ -150,6 +153,7 @@ private class BlockchainRowParameterProvider : CollectionPreviewParameterProvide
isSelected = false,
),
BlockchainRowUM(
id = "2",
name = "BNB BEACON CHAIN",
type = "1234567890111213141516171819",
iconResId = R.drawable.ic_bsc_16,

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
@Immutable
data class BlockchainRowUM(
val id: String,
val name: String,
val type: String,
val iconResId: Int,

View file

@ -1,8 +1,8 @@
package com.tangem.feature.wallet.presentation.common.component
package com.tangem.core.ui.components.token
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -16,15 +16,18 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Constraints
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.token.internal.*
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.component.token.*
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import org.burnoutcrew.reorderable.ReorderableLazyListState
import java.util.UUID
import kotlin.math.max
private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3
@ -34,12 +37,51 @@ private enum class LayoutId {
ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT
}
/**
* Token item for non reorderable list
*
* @param state token item state
* @param isBalanceHidden flag that shows/hides balance
* @param modifier modifier
*
* @see <a href = "https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1051-866&t=ew8mbGp2lacuJfFm-4"
* >Figma Component</a>
*/
@Composable
internal fun TokenItem(
fun TokenItem(
state: TokenItemState,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
reorderableTokenListState: ReorderableLazyListState? = null,
itemPaddingValues: PaddingValues = PaddingValues(horizontal = TangemTheme.dimens.spacing12),
) {
TokenItem(
state = state,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
reorderableTokenListState = null,
itemPaddingValues = itemPaddingValues,
)
}
/**
* Token item for reorderable list
*
* @param state token item state
* @param isBalanceHidden flag that shows/hides balance
* @param reorderableTokenListState reorderable token list state
* @param modifier modifier
* @param itemPaddingValues padding values
*
* @see <a href = "https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1051-866&t=ew8mbGp2lacuJfFm-4"
* >Figma Component</a>
*/
@Composable
fun TokenItem(
state: TokenItemState,
isBalanceHidden: Boolean,
reorderableTokenListState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
itemPaddingValues: PaddingValues = PaddingValues(horizontal = TangemTheme.dimens.spacing12),
) {
val betweenRowsMargin = TangemTheme.dimens.spacing2
@ -47,7 +89,7 @@ internal fun TokenItem(
state = state,
modifier = modifier
.tokenClickable(state = state)
.background(color = TangemTheme.colors.background.primary),
.padding(itemPaddingValues),
) {
CurrencyIcon(
state = state.iconState,
@ -73,7 +115,7 @@ internal fun TokenItem(
)
TokenPrice(
state = state.cryptoPriceState,
state = state.subtitleState,
modifier = Modifier
.layoutId(layoutId = LayoutId.CRYPTO_PRICE)
.padding(end = TangemTheme.dimens.spacing8),
@ -96,17 +138,22 @@ internal fun TokenItem(
@OptIn(ExperimentalFoundationApi::class)
private fun Modifier.tokenClickable(state: TokenItemState): Modifier = composed {
when (state) {
is TokenItemState.Content -> {
val onLongClick = rememberHapticFeedback(state = state, onAction = state.onItemLongClick)
combinedClickable(onClick = state.onItemClick, onLongClick = onLongClick)
}
is TokenItemState.Unreachable -> {
val onLongClick = rememberHapticFeedback(state = state, onAction = state.onItemLongClick)
combinedClickable(onClick = state.onItemClick, onLongClick = onLongClick)
}
is TokenItemState.NoAddress -> {
val onLongClick = rememberHapticFeedback(state = state, onAction = state.onItemLongClick)
combinedClickable(onClick = {}, onLongClick = onLongClick)
is TokenItemState.Content,
is TokenItemState.NoAddress,
is TokenItemState.Unreachable,
-> {
val onClick = state.onItemClick
val onLongClick = state.onItemLongClick?.let { rememberHapticFeedback(state = state, onAction = it) }
when {
onClick == null && onLongClick == null -> this
onClick == null && onLongClick != null -> combinedClickable(onClick = {}, onLongClick = onLongClick)
onClick != null && onLongClick == null -> combinedClickable(onClick = onClick)
onClick != null && onLongClick != null -> {
combinedClickable(onClick = onClick, onLongClick = onLongClick)
}
else -> this
}
}
is TokenItemState.Draggable,
is TokenItemState.Loading,
@ -127,10 +174,7 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
Layout(content = content, modifier = modifier) { measurables, constraints ->
val layoutWidth = constraints.maxWidth
val horizontalPadding = with(density) { dimens.size12.roundToPx() }
val verticalPadding = with(density) { dimens.size15.roundToPx() }
val layoutWidthWithoutPaddings = layoutWidth - 2 * horizontalPadding
val titleMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt()
val priceChangeMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt()
@ -166,32 +210,36 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
-> {
fiatAmount = measurables.measureFiatAmount(
state = state,
maxWidth = layoutWidthWithoutPaddings - icon.width - titleMinWidth,
maxWidth = layoutWidth - icon.width - titleMinWidth,
defaultConstraints = constraints,
)
cryptoAmount = measurables.measureCryptoAmount(
state = state,
maxWidth = layoutWidthWithoutPaddings - icon.width - priceChangeMinWidth,
maxWidth = layoutWidth - icon.width - priceChangeMinWidth,
defaultConstraints = constraints,
)
firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - fiatAmount.width
secondRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - cryptoAmount.width
firstRowRemainingFreeSpace = layoutWidth - icon.width - fiatAmount.width
secondRowRemainingFreeSpace = layoutWidth - icon.width - cryptoAmount.width
}
is TokenItemState.Draggable -> {
cryptoAmount = measurables.measureCryptoAmount(
state = state,
maxWidth = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width,
maxWidth = layoutWidth - icon.width - nonFiatContent.width,
defaultConstraints = constraints,
)
firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width
firstRowRemainingFreeSpace = layoutWidth - icon.width - nonFiatContent.width
}
is TokenItemState.NoAddress,
is TokenItemState.Unreachable,
-> {
firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width
firstRowRemainingFreeSpace = layoutWidth - icon.width - nonFiatContent.width
if (state.subtitleState != null) {
secondRowRemainingFreeSpace = layoutWidth - icon.width - nonFiatContent.width
}
}
}
@ -222,35 +270,41 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
)
layout(width = constraints.maxWidth, height = layoutHeight) {
icon.placeRelative(x = horizontalPadding, y = (layoutHeight - icon.height).div(other = 2))
icon.placeRelative(x = 0, y = (layoutHeight - icon.height).div(other = 2))
title.placeRelative(
x = horizontalPadding + icon.width,
x = icon.width,
y = when (state) {
is TokenItemState.NoAddress,
is TokenItemState.Unreachable,
-> (layoutHeight - title.height).div(other = 2)
-> {
if (state.subtitleState == null) {
(layoutHeight - title.height).div(other = 2)
} else {
verticalPadding
}
}
else -> verticalPadding
},
)
fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width - horizontalPadding, y = verticalPadding)
fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width, y = verticalPadding)
priceChange?.placeRelative(
x = horizontalPadding + icon.width,
x = icon.width,
y = layoutHeight - priceChange.height - verticalPadding,
)
cryptoAmount?.placeRelative(
x = when (state) {
is TokenItemState.Draggable -> horizontalPadding + icon.width
else -> layoutWidth - cryptoAmount.width - horizontalPadding
is TokenItemState.Draggable -> icon.width
else -> layoutWidth - cryptoAmount.width
},
y = layoutHeight - cryptoAmount.height - verticalPadding,
)
nonFiatContent.placeRelative(
x = layoutWidth - nonFiatContent.width - horizontalPadding,
x = layoutWidth - nonFiatContent.width,
y = (layoutHeight - nonFiatContent.height).div(other = 2),
)
}
@ -397,8 +451,8 @@ private fun Preview_TokenItem_InLight(@PreviewParameter(TokenItemStateProvider::
private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenItemState>(
collection = listOf(
WalletPreviewData.tokenItemVisibleState.copy(
iconState = WalletPreviewData.coinIconState.copy(showCustomBadge = true),
tokenItemVisibleState.copy(
iconState = coinIconState.copy(showCustomBadge = true),
titleState = TokenItemState.TitleState.Content(
text = "PolygonPolygonPolygonPolygonPolygonPolygon",
hasPending = true,
@ -408,19 +462,122 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
hasStaked = true,
),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,4123123213123123123123123123 MATIC"),
cryptoPriceState = TokenItemState.CryptoPriceState.Content(
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
price = "312 USD",
priceChangePercent = "42.0%",
type = PriceChangeType.UP,
),
),
WalletPreviewData.tokenItemUnreachableState,
WalletPreviewData.tokenItemNoAddressState,
WalletPreviewData.tokenItemDragState,
WalletPreviewData.tokenItemHiddenState,
WalletPreviewData.loadingTokenItemState,
WalletPreviewData.testnetTokenItemVisibleState,
WalletPreviewData.customTokenItemVisibleState,
WalletPreviewData.customTestnetTokenItemVisibleState,
TokenItemState.Unreachable(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
onItemClick = {},
onItemLongClick = {},
),
TokenItemState.Unreachable(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
subtitleState = TokenItemState.SubtitleState.TextContent("Token"),
onItemClick = {},
onItemLongClick = {},
),
TokenItemState.NoAddress(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
onItemLongClick = {},
),
TokenItemState.NoAddress(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
subtitleState = TokenItemState.SubtitleState.TextContent("Token"),
onItemLongClick = {},
),
TokenItemState.Draggable(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "3 172,14 $"),
),
TokenItemState.Content(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = false),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"),
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
price = "312 USD",
priceChangePercent = "2.0%",
type = PriceChangeType.UP,
),
onItemClick = {},
onItemLongClick = {},
),
TokenItemState.Loading(
id = "Loading#1",
iconState = customTokenIconState.copy(isGrayscale = true),
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
),
tokenItemVisibleState.copy(
titleState = TokenItemState.TitleState.Content(text = "Polygon testnet"),
iconState = tokenIconState.copy(isGrayscale = true),
),
tokenItemVisibleState.copy(
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
iconState = customTokenIconState.copy(
tint = TangemColorPalette.White,
background = TangemColorPalette.Black,
),
),
tokenItemVisibleState.copy(
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
iconState = customTokenIconState.copy(isGrayscale = true),
),
),
)
) {
companion object {
val coinIconState
get() = CurrencyIconState.CoinIcon(
url = null,
fallbackResId = R.drawable.img_polygon_22,
isGrayscale = false,
showCustomBadge = false,
)
val tokenIconState
get() = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_polygon_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
showCustomBadge = false,
)
private val customTokenIconState
get() = CurrencyIconState.CustomTokenIcon(
tint = TangemColorPalette.Black,
background = TangemColorPalette.Meadow,
topBadgeIconResId = R.drawable.img_polygon_22,
isGrayscale = false,
)
val tokenItemVisibleState by lazy {
TokenItemState.Content(
id = UUID.randomUUID().toString(),
iconState = coinIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon", hasPending = true),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = true),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"),
subtitleState = TokenItemState.SubtitleState.Unknown,
onItemClick = {},
onItemLongClick = {},
)
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.common.component.token
package com.tangem.core.ui.components.token.internal
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.common.component.token
package com.tangem.core.ui.components.token.internal
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedContent
@ -12,9 +12,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.common.component.token
package com.tangem.core.ui.components.token.internal
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -8,11 +8,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.utils.StringsSigns
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoAmountState as TokenCryptoAmountState
import com.tangem.core.ui.components.token.state.TokenItemState.CryptoAmountState as TokenCryptoAmountState
@Composable
internal fun TokenCryptoAmount(

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.common.component.token
package com.tangem.core.ui.components.token.internal
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Row
@ -14,11 +14,11 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.utils.StringsSigns
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.FiatAmountState as TokenFiatAmountState
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState
@Composable
internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.common.component.token
package com.tangem.core.ui.components.token.internal
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
@ -12,19 +13,23 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.SpacerW6
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoPriceState as TokenPriceChangeState
import com.tangem.core.ui.components.token.state.TokenItemState.SubtitleState as TokenPriceState
@Composable
internal fun TokenPrice(state: TokenPriceChangeState?, modifier: Modifier = Modifier) {
internal fun TokenPrice(state: TokenPriceState?, modifier: Modifier = Modifier) {
when (state) {
is TokenPriceChangeState.Content -> {
is TokenPriceState.CryptoPriceContent -> {
PriceBlock(
modifier = modifier,
price = state.price,
@ -32,13 +37,12 @@ internal fun TokenPrice(state: TokenPriceChangeState?, modifier: Modifier = Modi
priceChangePercent = state.priceChangePercent,
)
}
is TokenPriceChangeState.Unknown -> {
PriceText(text = DASH_SIGN, modifier = modifier)
}
is TokenPriceChangeState.Loading -> {
is TokenPriceState.TextContent -> PriceText(text = state.value, modifier = modifier)
is TokenPriceState.Unknown -> PriceText(text = DASH_SIGN, modifier = modifier)
is TokenPriceState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenPriceChangeState.Locked -> {
is TokenPriceState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize())
}
null -> Unit
@ -122,4 +126,37 @@ private fun Modifier.placeholderSize(): Modifier = composed {
return@composed this
.padding(vertical = TangemTheme.dimens.spacing2)
.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview(@PreviewParameter(TokenPriceChangeStateProvider::class) state: TokenPriceState) {
TangemThemePreview {
TokenPrice(state = state)
}
}
private class TokenPriceChangeStateProvider : CollectionPreviewParameterProvider<TokenPriceState>(
collection = listOf(
TokenPriceState.CryptoPriceContent(
price = "1.234",
priceChangePercent = "2.5%",
type = PriceChangeType.UP,
),
TokenPriceState.CryptoPriceContent(
price = "1.234",
priceChangePercent = "2.5%",
type = PriceChangeType.DOWN,
),
TokenPriceState.CryptoPriceContent(
price = "1.234",
priceChangePercent = "2.5%",
type = PriceChangeType.NEUTRAL,
),
TokenPriceState.TextContent(value = "Subtitle"),
TokenPriceState.Unknown,
TokenPriceState.Loading,
TokenPriceState.Locked,
),
)

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.common.component.token
package com.tangem.core.ui.components.token.internal
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
@ -13,10 +13,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TitleState as TokenTitleState
import com.tangem.core.ui.components.token.state.TokenItemState.TitleState as TokenTitleState
@Composable
internal fun TokenTitle(state: TokenTitleState?, modifier: Modifier = Modifier) {

View file

@ -0,0 +1,206 @@
package com.tangem.core.ui.components.token.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
/** TokenItem component state */
@Immutable
sealed class TokenItemState {
/** Unique id */
abstract val id: String
/** Token icon state */
abstract val iconState: CurrencyIconState
/** Token title state (in one row with [fiatAmountState]) */
abstract val titleState: TitleState
/** Token subtitle state (under [titleState] and in one row with [cryptoAmountState]) */
abstract val subtitleState: SubtitleState?
/** Token fiat amount state (in one row with [titleState]) */
abstract val fiatAmountState: FiatAmountState?
/** Token crypto amount state (under [fiatAmountState] and in one row with [subtitleState]) */
abstract val cryptoAmountState: CryptoAmountState?
/** Callback which will be called when an item is clicked */
abstract val onItemClick: (() -> Unit)?
/** Callback which will be called when an item is long clicked */
abstract val onItemLongClick: (() -> Unit)?
/**
* Loading token state
*
* @property id unique id
* @property iconState token icon state
* @property titleState token title
* @property subtitleState token subtitle
*/
data class Loading(
override val id: String,
override val iconState: CurrencyIconState,
override val titleState: TitleState.Content,
override val subtitleState: SubtitleState = SubtitleState.Loading,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState = FiatAmountState.Loading
override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Loading
override val onItemClick: (() -> Unit)? = null
override val onItemLongClick: (() -> Unit)? = null
}
/**
* Locked token state
*
* @property id unique id
*/
data class Locked(override val id: String) : TokenItemState() {
override val iconState: CurrencyIconState = CurrencyIconState.Locked
override val titleState: TitleState = TitleState.Locked
override val subtitleState: SubtitleState = SubtitleState.Locked
override val fiatAmountState: FiatAmountState = FiatAmountState.Locked
override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked
override val onItemClick: (() -> Unit)? = null
override val onItemLongClick: (() -> Unit)? = null
}
/**
* Content token state
*
* @property id unique id
* @property iconState token icon state
* @property titleState token title
* @property subtitleState token subtitle
* @property fiatAmountState token fiat amount
* @property cryptoAmountState token crypto amount
* @property onItemClick callback which will be called when an item is clicked
* @property onItemLongClick callback which will be called when an item is long clicked
*/
data class Content(
override val id: String,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
override val subtitleState: SubtitleState,
override val fiatAmountState: FiatAmountState,
override val cryptoAmountState: CryptoAmountState.Content,
override val onItemClick: (() -> Unit)?,
override val onItemLongClick: (() -> Unit)?,
) : TokenItemState()
/**
* Draggable token state
*
* @property id unique id
* @property iconState token icon state
* @property titleState token title
* @property cryptoAmountState token crypto amount
*/
data class Draggable(
override val id: String,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
override val cryptoAmountState: CryptoAmountState,
) : TokenItemState() {
override val subtitleState: SubtitleState? = null
override val fiatAmountState: FiatAmountState? = null
override val onItemClick: (() -> Unit)? = null
override val onItemLongClick: (() -> Unit)? = null
}
/**
* Unreachable token state
*
* @property id unique id
* @property iconState token icon state
* @property titleState token title
* @property subtitleState token subtitle
* @property onItemClick callback which will be called when an item is clicked
* @property onItemLongClick callback which will be called when an item is long clicked
*/
data class Unreachable(
override val id: String,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
override val subtitleState: SubtitleState? = null,
override val onItemClick: (() -> Unit)?,
override val onItemLongClick: (() -> Unit)?,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState? = null
override val cryptoAmountState: CryptoAmountState? = null
}
/**
* No derivation address state
*
* @property id unique id
* @property iconState token icon state
* @property titleState token title
* @property subtitleState token subtitle
* @property onItemLongClick callback which will be called when an item is long clicked
*/
data class NoAddress(
override val id: String,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
override val subtitleState: SubtitleState? = null,
override val onItemLongClick: (() -> Unit)?,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState? = null
override val cryptoAmountState: CryptoAmountState? = null
override val onItemClick: (() -> Unit)? = null
}
@Immutable
sealed class TitleState {
data class Content(val text: String, val hasPending: Boolean = false) : TitleState()
data object Loading : TitleState()
data object Locked : TitleState()
}
@Immutable
sealed class SubtitleState {
data class CryptoPriceContent(
val price: String,
val priceChangePercent: String,
val type: PriceChangeType,
) : SubtitleState()
data class TextContent(val value: String) : SubtitleState()
data object Unknown : SubtitleState()
data object Loading : SubtitleState()
data object Locked : SubtitleState()
}
@Immutable
sealed class FiatAmountState {
data class Content(
val text: String,
val hasStaked: Boolean = false,
) : FiatAmountState()
data object Loading : FiatAmountState()
data object Locked : FiatAmountState()
}
@Immutable
sealed class CryptoAmountState {
data class Content(val text: String) : CryptoAmountState()
data object Unreachable : CryptoAmountState()
data object Loading : CryptoAmountState()
data object Locked : CryptoAmountState()
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.core.ui.decompose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@Stable
interface ComposableBottomSheetComponent {
fun dismiss()
@Composable
fun BottomSheet()
}

View file

@ -73,6 +73,7 @@ fun getActiveIconRes(blockchainId: String): Int {
"blast", "blast/test" -> R.drawable.img_blast_22
"filecoin" -> R.drawable.img_filecoin_22
"cyber", "cyber/test" -> R.drawable.img_cyber_22
"sei", "sei/test" -> R.drawable.img_sei_22
else -> R.drawable.ic_alert_24
}
}
@ -147,6 +148,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int {
"blast", "blast/test" -> R.drawable.img_blast_22
"filecoin" -> R.drawable.img_filecoin_22
"cyber", "cyber/test" -> R.drawable.img_cyber_22
"sei", "sei/test" -> R.drawable.img_sei_22
else -> R.drawable.ic_alert_24
}
}
@ -218,6 +220,7 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"blast", "blast/test" -> R.drawable.img_blast_22
"filecoin" -> R.drawable.img_filecoin_22
"cyber", "cyber/test" -> R.drawable.img_cyber_22
"sei", "sei/test" -> R.drawable.img_sei_22
else -> R.drawable.ic_alert_24
}
}
@ -292,6 +295,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"blast", "blast/test" -> R.drawable.ic_blast_22
"filecoin" -> R.drawable.ic_filecoin_22
"cyber", "cyber/test" -> R.drawable.ic_cyber_22
"sei", "sei/test" -> R.drawable.ic_sei_22
else -> R.drawable.ic_alert_24
}
}
@ -366,6 +370,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int {
"blast", "blast/test" -> R.drawable.ic_blast_22
"filecoin" -> R.drawable.ic_filecoin_22
"cyber", "cyber/test" -> R.drawable.ic_cyber_22
"sei", "sei/test" -> R.drawable.ic_sei_22
else -> R.drawable.ic_alert_24
}
}

Some files were not shown because too many files have changed in this diff Show more