Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-20 16:16:25 +00:00
commit 31da96588b
473 changed files with 11011 additions and 9716 deletions

View file

@ -20,6 +20,9 @@ configurations.all {
substitute(module("com.facebook.react:hermes-engine"))
.using(module("com.facebook.react:hermes-android:0.72.4"))
substitute(module("org.bouncycastle:bcprov-jdk15on"))
.using(module("org.bouncycastle:bcprov-jdk18on:1.73"))
}
force(

View file

@ -120,7 +120,7 @@
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="success.tangem.com"
android:host="redirect"
android:scheme="tangem" />
</intent-filter>

View file

@ -434,6 +434,16 @@
}
]
},
{
"id" : "algorand",
"symbol" : "ALGO",
"name" : "Algorand",
"networks" : [
{
"networkId" : "algorand/test"
}
]
},
{
"id" : "arbitrum-one",
"symbol" : "ETH",
@ -569,6 +579,16 @@
}
]
},
{
"id": "shibarium",
"name": "Shibarium",
"symbol": "BONE",
"networks": [
{
"networkId": "shibarium/test"
}
]
},
{
"id": "aptos",
"name": "Aptos",
@ -578,6 +598,16 @@
"networkId": "aptos/test"
}
]
},
{
"id": "hedera-hashgraph",
"name": "Hedera",
"symbol": "HBAR",
"networks": [
{
"networkId": "hedera-hashgraph/test"
}
]
}
]
}

View file

@ -37,7 +37,7 @@ import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.features.managetokens.navigation.ManageTokensRouter
import com.tangem.features.managetokens.navigation.ManageTokensUi
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
@ -126,7 +126,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
lateinit var tokenDetailsRouter: TokenDetailsRouter
@Inject
lateinit var manageTokensRouter: ManageTokensRouter
lateinit var manageTokensUi: ManageTokensUi
@Inject
lateinit var walletConnectInteractor: WalletConnectInteractor
@ -213,7 +213,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
walletRouter = walletRouter,
walletConnectInteractor = walletConnectInteractor,
tokenDetailsRouter = tokenDetailsRouter,
manageTokensRouter = manageTokensRouter,
manageTokensUi = manageTokensUi,
cardSdkConfigRepository = cardSdkConfigRepository,
sendRouter = sendRouter,
qrScanningRouter = qrScanningRouter,

View file

@ -10,6 +10,8 @@ import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.filter.OneTimeEventFilter
@ -28,7 +30,6 @@ import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.common.LogConfig
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
@ -37,10 +38,8 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
@ -118,9 +117,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var scanCardProcessor: ScanCardProcessor
@Inject
lateinit var blockchainExceptionHandler: BlockchainExceptionHandler
@Inject
lateinit var appCurrencyRepository: AppCurrencyRepository
@ -155,10 +151,10 @@ internal class TapApplication : Application(), ImageLoaderFactory {
lateinit var oneTimeEventFilter: OneTimeEventFilter
@Inject
lateinit var derivationsRepository: DerivationsRepository
lateinit var blockchainDataStorage: BlockchainDataStorage
@Inject
lateinit var testerFeatureToggles: TesterFeatureToggles
lateinit var accountCreator: AccountCreator
// endregion Injected
override fun onCreate() {
@ -234,8 +230,8 @@ internal class TapApplication : Application(), ImageLoaderFactory {
balanceHidingRepository = balanceHidingRepository,
walletsRepository = walletsRepository,
sendFeatureToggles = sendFeatureToggles,
derivationsRepository = derivationsRepository,
testerFeatureToggles = testerFeatureToggles,
blockchainDataStorage = blockchainDataStorage,
accountCreator = accountCreator,
),
),
)

View file

@ -1,6 +1,8 @@
package com.tangem.tap.common
import android.content.Context
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.Intent.FLAG_ACTIVITY_NO_HISTORY
import android.net.Uri
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
@ -22,6 +24,10 @@ class CustomTabsManager {
if (MutableAppThemeModeHolder.isDarkThemeActive) COLOR_SCHEME_DARK else COLOR_SCHEME_LIGHT,
)
.build()
// Open CustomTabsActivity as new task without saving into the stack
customTabsIntent.intent.setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_NO_HISTORY)
customTabsIntent.launchUrl(context, Uri.parse(url))
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.extensions.filterNotNull
/**
[REDACTED_AUTHOR]
@ -42,17 +41,22 @@ sealed class WalletConnect(
val methodName: String,
val blockchain: String,
val errorCode: String? = null,
val errorDescription: String? = null,
) {
fun toParamsMap(): Map<String, String> {
val validation = if (errorCode == null) Validation.SUCCESS.param else Validation.FAIL.param
return mapOf(
AnalyticsParam.DAPP_NAME to dAppName,
AnalyticsParam.DAPP_URL to dAppUrl,
AnalyticsParam.METHOD_NAME to methodName,
AnalyticsParam.BLOCKCHAIN to blockchain,
AnalyticsParam.VALIDATION to validation,
if (errorCode != null) AnalyticsParam.ERROR_CODE to errorCode else null to null,
).filterNotNull()
val code = errorCode ?: SUCCESS_CODE
return buildMap {
put(AnalyticsParam.DAPP_NAME, dAppName)
put(AnalyticsParam.DAPP_URL, dAppUrl)
put(AnalyticsParam.METHOD_NAME, methodName)
put(AnalyticsParam.BLOCKCHAIN, blockchain)
put(AnalyticsParam.VALIDATION, validation)
put(AnalyticsParam.ERROR_CODE, code)
if (errorDescription != null) {
put(AnalyticsParam.ERROR_DESCRIPTION, errorDescription)
}
}
}
}
@ -60,4 +64,8 @@ sealed class WalletConnect(
SUCCESS("Success"),
FAIL("Fail"),
}
companion object {
private const val SUCCESS_CODE = "0"
}
}

View file

@ -151,18 +151,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.ResetToFactory -> ResetCardFragment()
AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.ManageTokens -> {
val featureToggles = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::manageTokensFeatureToggles,
)
if (featureToggles.isRedesignedScreenEnabled) {
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::manageTokensRouter)
.getEntryFragment()
} else {
TokensListFragment()
}
}
AppScreen.ManageTokens -> TokensListFragment()
AppScreen.AddCustomToken -> AddCustomTokenFragment()
AppScreen.WalletDetails -> {
store.state.daggerGraphState

View file

@ -33,7 +33,6 @@ import com.tangem.tap.features.shop.redux.ShopMiddleware
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.features.signin.redux.SignInMiddleware
import com.tangem.tap.features.signin.redux.SignInState
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@ -91,7 +90,6 @@ data class AppState(
SendMiddleware().sendMiddleware,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware.tokensMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
ShopMiddleware().shopMiddleware,

View file

@ -77,7 +77,7 @@ class TruncateStart : BaseTruncate() {
class TruncateMiddle : BaseTruncate() {
override fun roughTruncate(text: String, residualLength: Int): String {
if (text.length <= residualLength) return text
if (text.length <= residualLength || residualLength < 0) return text
hasBeenTruncated = true
val halfOfResidualLength = residualLength / 2

View file

@ -8,8 +8,6 @@ import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.card.DefaultDerivePublicKeysUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -62,14 +60,8 @@ internal object CardDomainModule {
@Provides
@ViewModelScoped
fun provideDerivePublicKeysUseCase(
tangemSdkManager: TangemSdkManager,
derivationsRepository: DerivationsRepository,
): DerivePublicKeysUseCase {
return DefaultDerivePublicKeysUseCase(
tangemSdkManager = tangemSdkManager,
derivationsRepository = derivationsRepository,
)
fun provideDerivePublicKeysUseCase(derivationsRepository: DerivationsRepository): DerivePublicKeysUseCase {
return DerivePublicKeysUseCase(derivationsRepository = derivationsRepository)
}
@Provides

View file

@ -99,6 +99,7 @@ internal object TokensDomainModule {
networksRepository: NetworksRepository,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
swapRepository: SwapRepository,
currencyChecksRepository: CurrencyChecksRepository,
showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
promoRepository: PromoRepository,
dispatchers: CoroutineDispatcherProvider,
@ -109,6 +110,7 @@ internal object TokensDomainModule {
quotesRepository = quotesRepository,
networksRepository = networksRepository,
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
currencyChecksRepository = currencyChecksRepository,
swapRepository = swapRepository,
showSwapPromoTokenUseCase = showSwapPromoTokenUseCase,
promoRepository = promoRepository,

View file

@ -1,18 +1,34 @@
package com.tangem.tap.di.domain
import com.tangem.domain.visa.GetVisaCurrencyUseCase
import com.tangem.domain.visa.GetVisaTxDetailsUseCase
import com.tangem.domain.visa.GetVisaTxHistoryUseCase
import com.tangem.domain.visa.repository.VisaRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
internal object VisaDomainModule {
@Provides
@ViewModelScoped
fun provideVisaCurrencyUseCase(visaRepository: VisaRepository): GetVisaCurrencyUseCase {
return GetVisaCurrencyUseCase(visaRepository)
}
@Provides
@ViewModelScoped
fun provideGetVisaTxHistoryUseCase(visaRepository: VisaRepository): GetVisaTxHistoryUseCase {
return GetVisaTxHistoryUseCase(visaRepository)
}
@Provides
@ViewModelScoped
fun provideGetVisaTxDetailsUseCase(visaRepository: VisaRepository): GetVisaTxDetailsUseCase {
return GetVisaTxDetailsUseCase(visaRepository)
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.tap.di.domain
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.di.SdkMoshi
@ -25,6 +27,8 @@ internal object WalletManagersFacadeModule {
walletManagersStore: WalletManagersStore,
userWalletsStore: UserWalletsStore,
configManager: ConfigManager,
blockchainDataStorage: BlockchainDataStorage,
accountCreator: AccountCreator,
mnemonicRepository: MnemonicRepository,
assetReader: AssetReader,
@SdkMoshi moshi: Moshi,
@ -33,9 +37,11 @@ internal object WalletManagersFacadeModule {
walletManagersStore = walletManagersStore,
userWalletsStore = userWalletsStore,
configManager = configManager,
blockchainDataStorage = blockchainDataStorage,
assetReader = assetReader,
moshi = moshi,
mnemonic = mnemonicRepository.generateDefaultMnemonic(),
accountCreator = accountCreator,
)
}
}

View file

@ -5,19 +5,20 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.operations.attestation.Attestation
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -40,8 +41,13 @@ class TapWalletManager(
field = value
}
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
val walletManagerFactory: WalletManagerFactory by lazy {
WalletManagerFactory(
config = blockchainSdkConfig,
accountCreator = store.state.daggerGraphState.get(DaggerGraphState::accountCreator),
blockchainDataStorage = store.state.daggerGraphState.get(DaggerGraphState::blockchainDataStorage),
)
}
suspend fun onWalletSelected(userWallet: UserWallet, sendAnalyticsEvent: Boolean) {
// If a previous job was running, it gets cancelled before the new one starts,

View file

@ -1,41 +0,0 @@
package com.tangem.tap.domain.card
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.tap.domain.TangemSdkManager
internal class DefaultDerivePublicKeysUseCase(
private val tangemSdkManager: TangemSdkManager,
private val derivationsRepository: DerivationsRepository,
) : DerivePublicKeysUseCase {
override suspend fun invoke(
cardId: String?,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
): Either<Unit, DerivationTaskResponse> {
tangemSdkManager.derivePublicKeys(cardId = cardId, derivations = derivations)
.doOnSuccess { return it.right() }
.doOnFailure { return Unit.left() }
return Unit.left()
}
override suspend fun invoke(
userWalletId: UserWalletId,
currencies: List<CryptoCurrency>,
): Either<Throwable, Unit> {
return Either.catch {
derivationsRepository.derivePublicKeys(userWalletId, currencies)
}
}
}

View file

@ -352,7 +352,8 @@ class WalletConnectRepositoryImpl @Inject constructor(
dAppUrl = session?.url ?: "",
methodName = requestData.method,
blockchain = requestData.blockchain,
errorCode = WalletConnectError.ValidationError.toString(),
errorCode = WalletConnectError.ValidationError.error,
errorDescription = it.throwable.message,
)
},
)

View file

@ -1,12 +1,24 @@
package com.tangem.tap.domain.walletconnect2.domain.models
sealed class WalletConnectError : Exception() {
data class ApprovalErrorMissingNetworks(val missingChains: List<String>) : WalletConnectError()
data class ApprovalErrorAddNetwork(val networks: List<String>) : WalletConnectError()
data class ApprovalErrorUnsupportedNetwork(val unsupportedNetworks: List<String>) : WalletConnectError()
data class ExternalApprovalError(override val message: String?) : WalletConnectError()
object WrongUserWallet : WalletConnectError()
object UnsupportedMethod : WalletConnectError()
object SigningError : WalletConnectError()
object ValidationError : WalletConnectError()
sealed class WalletConnectError(val error: String) : Exception() {
data class ApprovalErrorMissingNetworks(
val missingChains: List<String>,
) : WalletConnectError("ApprovalErrorMissingNetworks")
data class ApprovalErrorAddNetwork(
val networks: List<String>,
) : WalletConnectError("ApprovalErrorAddNetwork")
data class ApprovalErrorUnsupportedNetwork(
val unsupportedNetworks: List<String>,
) : WalletConnectError("ApprovalErrorUnsupportedNetwork")
data class ExternalApprovalError(
override val message: String?,
) : WalletConnectError("ExternalApprovalError")
object WrongUserWallet : WalletConnectError("WrongUserWallet")
object UnsupportedMethod : WalletConnectError("UnsupportedMethod")
object SigningError : WalletConnectError("SigningError")
object ValidationError : WalletConnectError("ValidationError")
}

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.customtoken.impl.di
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.tap.features.customtoken.impl.data.DefaultCustomTokenRepository
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
import com.tangem.tap.features.customtoken.impl.domain.DefaultCustomTokenInteractor
@ -30,7 +29,6 @@ internal object CustomTokenInteractorModule {
reduxStateHolder: AppStateHolder,
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
derivePublicKeysUseCase: DerivePublicKeysUseCase,
testerFeatureToggles: TesterFeatureToggles,
): CustomTokenInteractor {
return DefaultCustomTokenInteractor(
featureRepository = DefaultCustomTokenRepository(
@ -40,7 +38,6 @@ internal object CustomTokenInteractorModule {
),
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
derivePublicKeysUseCase = derivePublicKeysUseCase,
testerFeatureToggles = testerFeatureToggles,
)
}
}

View file

@ -1,40 +1,19 @@
package com.tangem.tap.features.customtoken.impl.domain
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.TangemError
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import timber.log.Timber
/**
@ -48,7 +27,6 @@ class DefaultCustomTokenInteractor(
private val featureRepository: CustomTokenRepository,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val testerFeatureToggles: TesterFeatureToggles,
) : CustomTokenInteractor {
// TODO: Move to DI
@ -70,136 +48,12 @@ class DefaultCustomTokenInteractor(
val userWallet = getSelectedWalletSyncUseCase().fold(ifLeft = { return }, ifRight = { it })
val currency = Currency.fromCustomCurrency(customCurrency)
if (testerFeatureToggles.isDerivePublicKeysRefactoringEnabled) {
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies)
.onRight {
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies)
}
.onLeft { Timber.e("Failed to derive public keys: $it") }
} else {
// TODO: delete [REDACTED_JIRA]
val isNeedToDerive = isNeedToDerive(userWallet, currency)
if (isNeedToDerive) {
deriveMissingBlockchains(
userWallet = userWallet,
currencyList = listOf(currency),
onSuccess = { submitAdd(userWallet = userWallet.copy(scanResponse = it), currency = currency) },
) {
throw it
}
} else {
submitAdd(userWallet, currency)
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies)
.onRight {
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies)
}
}
}
private fun isNeedToDerive(userWallet: UserWallet, currency: Currency): Boolean {
val scanResponse = userWallet.scanResponse
return currency.derivationPath?.let { !scanResponse.hasDerivation(currency.blockchain, it) } ?: false
}
private suspend fun deriveMissingBlockchains(
userWallet: UserWallet,
currencyList: List<Currency>,
onSuccess: suspend (ScanResponse) -> Unit,
onFailure: suspend (TangemError) -> Unit,
) {
val scanResponse = userWallet.scanResponse
val config = CardConfig.createConfig(scanResponse.card)
val derivationDataList = currencyList.mapNotNull { currency ->
val curve = config.primaryCurve(currency.blockchain)
curve?.let { getDerivations(curve, scanResponse, currency) }
}
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
derivationDataList.forEach {
val current = this[it.derivations.first]
if (current != null) {
current.addAll(it.derivations.second)
current.distinct()
} else {
this[it.derivations.first] = it.derivations.second.toMutableList()
}
}
}
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
when (val result = tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data.entries
val oldDerivedKeys = scanResponse.derivedKeys
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys)
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
onFailure.invoke(result.error)
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
}
}
}
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: Currency,
): TokensMiddleware.DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val supportedCurves = currency.blockchain.getSupportedCurves()
val path = currency.blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
.takeIf { supportedCurves.contains(curve) }
val customPath = currency.derivationPath?.let {
DerivationPath(it)
}.takeIf { supportedCurves.contains(curve) }
val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
if (currency is Currency.Blockchain && currency.blockchain == Blockchain.Cardano) {
currency.derivationPath?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
private suspend fun submitAdd(userWallet: UserWallet, currency: Currency) {
val scanResponse = userWallet.scanResponse
val userWalletId = userWallet.walletId
val currencyList = listOfNotNull(element = currency.toCryptoCurrency(scanResponse))
userWalletsListManager.update(userWalletId) {
it.copy(scanResponse = scanResponse)
}
addCryptoCurrenciesUseCase(userWalletId, currencyList)
.onLeft { Timber.e("Failed to derive public keys: $it") }
}
private fun Currency.toCryptoCurrency(scanResponse: ScanResponse): CryptoCurrency? {

View file

@ -3,21 +3,21 @@ package com.tangem.tap.features.customtoken.impl.presentation.validators
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.AddressService
import com.tangem.common.card.EllipticCurve
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.tokens.error.AddCustomTokenError
/**
* Validator of contract address
*
[REDACTED_AUTHOR]
*/
object ContactAddressValidator {
object ContractAddressValidator {
/** Validate a [address] using [blockchain] */
fun validate(address: String, blockchain: Blockchain): ContractAddressValidatorResult {
return when {
address.isEmpty() -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.FieldIsEmpty)
address.isEmpty() -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.FIELD_IS_EMPTY)
validateAddress(blockchain, address) -> ContractAddressValidatorResult.Success
else -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.InvalidContractAddress)
else -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.INVALID_CONTRACT_ADDRESS)
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.tap.features.customtoken.impl.presentation.validators
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.tokens.error.AddCustomTokenError
/**
* Result of validation contract address

View file

@ -16,13 +16,13 @@ import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.HDWalletError
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.*
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.error.AddCustomTokenError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
@ -33,7 +33,7 @@ import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTok
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField.SelectorItem
import com.tangem.tap.features.customtoken.impl.presentation.routers.CustomTokenRouter
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContactAddressValidator
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidator
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidatorResult
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -461,11 +461,11 @@ internal class AddCustomTokenViewModel @Inject constructor(
private fun getTokenWarningSet(): Set<AddCustomTokenWarning> {
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
val isContractAddressFieldEmpty = ContactAddressValidator.validate(
val isContractAddressFieldEmpty = ContractAddressValidator.validate(
address = uiState.form.contractAddressInputField.value,
blockchain = networkSelectorValue,
).let {
it is ContractAddressValidatorResult.Error && it.type == AddCustomTokenError.FieldIsEmpty
it is ContractAddressValidatorResult.Error && it.type == AddCustomTokenError.FIELD_IS_EMPTY
}
val isSupportedToken = if (!isNetworkSelected()) {
@ -513,7 +513,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
val state = when {
isAllTokenFieldsFilled() && isNetworkSelected() -> {
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
val error = ContactAddressValidator.validate(
val error = ContractAddressValidator.validate(
address = uiState.form.contractAddressInputField.value,
blockchain = networkSelectorValue,
)
@ -621,7 +621,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
private fun handleContractAddressErrorValidation(type: AddCustomTokenError) {
when {
isNetworkSelected() && type == AddCustomTokenError.InvalidContractAddress -> {
isNetworkSelected() && type == AddCustomTokenError.INVALID_CONTRACT_ADDRESS -> {
val isAnotherTokenFieldsFilled = isAnyTokenFieldsFilled()
uiState = uiState.copySealed(
form = uiState.form.copy(
@ -644,7 +644,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
)
}
!isNetworkSelected() || type == AddCustomTokenError.FieldIsEmpty -> {
!isNetworkSelected() || type == AddCustomTokenError.FIELD_IS_EMPTY -> {
uiState = uiState.copySealed(
form = uiState.form.copy(
contractAddressInputField = uiState.form.contractAddressInputField.copy(isError = false),
@ -727,7 +727,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
)
val selectedNetwork = uiState.form.networkSelectorField.selectedItem.blockchain
val validatorResult = ContactAddressValidator.validate(
val validatorResult = ContractAddressValidator.validate(
address = enteredValue,
blockchain = selectedNetwork,
)

View file

@ -170,7 +170,7 @@ private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: M
.padding(horizontal = TangemTheme.dimens.spacing16)
.align(Alignment.BottomCenter)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
AnimatedVisibility(
visible = config.currentStory == Stories.Currencies,

View file

@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ButtonColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -13,15 +12,11 @@ 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.SpacerW8
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.wallet.R
@Suppress("MagicNumber")
@Composable
internal fun HomeButtons(
btnScanStateInProgress: Boolean,
@ -38,7 +33,7 @@ internal fun HomeButtons(
showProgress = btnScanStateInProgress,
onClick = onScanButtonClick,
)
SpacerW8()
SpacerW12()
OrderCardButton(
modifier = Modifier.weight(weight = 1f),
onClick = onShopButtonClick,
@ -48,44 +43,26 @@ internal fun HomeButtons(
@Composable
private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
TangemButton(
StoriesButton(
modifier = modifier,
text = stringResource(id = R.string.home_button_scan),
useDarkerColors = false,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
colors = LightBgScanCardButtonColors,
showProgress = showProgress,
enabled = true,
onClick = onClick,
showProgress = showProgress,
)
}
@Composable
private fun OrderCardButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
TangemButton(
StoriesButton(
modifier = modifier,
text = stringResource(id = R.string.home_button_order),
icon = TangemButtonIconPosition.None,
colors = LightBgOrderCardButtonColors,
showProgress = false,
enabled = true,
useDarkerColors = true,
onClick = onClick,
)
}
private val LightBgScanCardButtonColors: ButtonColors = TangemButtonColors(
backgroundColor = TangemColorPalette.Light4,
contentColor = TangemColorPalette.Dark6,
disabledBackgroundColor = TangemColorPalette.Dark5,
disabledContentColor = TangemColorPalette.Dark6,
)
private val LightBgOrderCardButtonColors: ButtonColors = TangemButtonColors(
backgroundColor = TangemColorPalette.Dark6,
contentColor = TangemColorPalette.White,
disabledBackgroundColor = TangemColorPalette.Dark6,
disabledContentColor = TangemColorPalette.White,
)
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
@ -95,10 +72,10 @@ private fun HomeButtonsPreview(@PreviewParameter(HomeButtonsParameterProvider::c
modifier = Modifier.background(Color.Black),
) {
HomeButtons(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
btnScanStateInProgress = state.btnScanStateInProgress,
onScanButtonClick = {},
onShopButtonClick = {},
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
)
}
}

View file

@ -1,31 +1,42 @@
package com.tangem.tap.features.home.compose.views
import androidx.compose.material.ButtonColors
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.wallet.R
@Composable
internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
TangemButton(
StoriesButton(
modifier = modifier,
text = stringResource(id = R.string.common_search_tokens),
icon = TangemButtonIconPosition.Start(R.drawable.ic_search_24),
onClick = onClick,
colors = SearchCurrenciesButtonColors,
showProgress = false,
enabled = true,
useDarkerColors = true,
onClick = onClick,
)
}
private val SearchCurrenciesButtonColors: ButtonColors = TangemButtonColors(
backgroundColor = TangemColorPalette.Dark5,
contentColor = TangemColorPalette.White,
disabledBackgroundColor = TangemColorPalette.Dark5,
disabledContentColor = TangemColorPalette.White,
)
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SearchCurrenciesButtonPreview() {
TangemTheme {
Box(
modifier = Modifier
.background(color = Color.Black)
.padding(all = TangemTheme.dimens.spacing16),
) {
SearchCurrenciesButton(modifier = Modifier.fillMaxWidth(), onClick = {})
}
}
}
// endregion Preview

View file

@ -0,0 +1,51 @@
package com.tangem.tap.features.home.compose.views
import androidx.compose.material.ButtonColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun StoriesButton(
text: String,
useDarkerColors: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
icon: TangemButtonIconPosition = TangemButtonIconPosition.None,
showProgress: Boolean = false,
) {
TangemButton(
modifier = modifier,
text = text,
icon = icon,
colors = if (useDarkerColors) DarkerButtonColors else LighterButtonColors,
showProgress = showProgress,
enabled = true,
shape = TangemTheme.shapes.roundedCornersXMedium,
iconPadding = when (icon) {
is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing4
is TangemButtonIconPosition.End,
is TangemButtonIconPosition.None,
-> TangemTheme.dimens.spacing8
},
onClick = onClick,
)
}
private val LighterButtonColors: ButtonColors = TangemButtonColors(
backgroundColor = TangemColorPalette.Light4,
contentColor = TangemColorPalette.Dark6,
disabledBackgroundColor = TangemColorPalette.Dark5,
disabledContentColor = TangemColorPalette.Dark6,
)
private val DarkerButtonColors: ButtonColors = TangemButtonColors(
backgroundColor = TangemColorPalette.Dark4,
contentColor = TangemColorPalette.White,
disabledBackgroundColor = TangemColorPalette.Dark4,
disabledContentColor = TangemColorPalette.White,
)

View file

@ -13,10 +13,10 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import kotlinx.coroutines.delay
@ -30,7 +30,7 @@ fun StoriesProgressBar(
stepDuration: Int = 8_000,
onStepFinish: () -> Unit = {},
) {
val progress = remember(currentStep) { Animatable(0f) }
val progress = remember(currentStep) { Animatable(initialValue = 0f) }
val context = LocalContext.current
val animatorSpeed = Settings.Global.getFloat(
@ -75,20 +75,21 @@ fun StoriesProgressBar(
.height(TangemTheme.dimens.size2)
.weight(1f)
.clip(RoundedCornerShape(TangemTheme.dimens.radius2))
.background(Color.White.copy(alpha = 0.4f)),
.background(TangemColorPalette.White.copy(alpha = .2f)),
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius2))
.background(Color.White)
.fillMaxHeight().let {
.background(TangemColorPalette.White)
.fillMaxHeight()
.let {
when (index) {
currentStep -> it.fillMaxWidth(progress.value)
in 0..currentStep -> it.fillMaxWidth(1f)
in 0..currentStep -> it.fillMaxWidth(fraction = 1f)
else -> it
}
},
) {}
)
}
if (index != steps) {
SpacerW4()
@ -100,5 +101,12 @@ fun StoriesProgressBar(
@Preview
@Composable
private fun StoriesProgressBarPreview() {
StoriesProgressBar(steps = 3, currentStep = 2, paused = false) { }
Box(
modifier = Modifier
.wrapContentSize()
.background(TangemColorPalette.Black)
.padding(vertical = TangemTheme.dimens.spacing16),
) {
StoriesProgressBar(steps = 5, currentStep = 3, paused = false)
}
}

View file

@ -59,26 +59,41 @@ sealed class TransactionExtrasAction : SendScreenActionUi {
object Release : TransactionExtrasAction()
@Deprecated("Only in legacy send screen")
sealed class XlmMemo : TransactionExtrasAction() {
// data class ChangeSelectedMemo(val memoType: XlmMemoType) : XlmMemo()
data class HandleUserInput(val data: String) : XlmMemo()
}
@Deprecated("Only in legacy send screen")
sealed class BinanceMemo : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : BinanceMemo()
}
@Deprecated("Only in legacy send screen")
sealed class XrpDestinationTag : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : XrpDestinationTag()
}
@Deprecated("Only in legacy send screen")
sealed class TonMemo : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : TonMemo()
}
@Deprecated("Only in legacy send screen")
sealed class CosmosMemo : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : CosmosMemo()
}
@Deprecated("Only in legacy send screen")
sealed class HederaMemo : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : HederaMemo()
}
@Deprecated("Only in legacy send screen")
sealed class AlgorandMemo : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : AlgorandMemo()
}
}
sealed class AddressVerifyAction : SendScreenAction {

View file

@ -1,8 +1,10 @@
package com.tangem.tap.features.send.redux.middlewares
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras
import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras
import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
@ -171,6 +173,12 @@ private fun sendTransaction(
transactionExtras.xrpDestinationTag?.tag?.let { txData = txData.copy(extras = XrpTransactionExtras(it)) }
transactionExtras.cosmosMemoState?.memo?.let { txData = txData.copy(extras = CosmosTransactionExtras(it)) }
transactionExtras.tonMemoState?.memo?.let { txData = txData.copy(extras = TonTransactionExtras(it)) }
transactionExtras.hederaMemoState?.memo?.let {
txData = txData.copy(
extras = HederaTransactionBuilder.HederaTransactionExtras(it),
)
}
transactionExtras.algorandMemoState?.memo?.let { txData = txData.copy(extras = AlgorandTransactionExtras(it)) }
scope.launch {
// TODO: Risky commented this part, unknown logic, need to test if removed

View file

@ -19,6 +19,8 @@ class TransactionExtrasReducer : SendInternalReducer {
is XrpDestinationTag -> handleXrpTag(action, sendState, sendState.transactionExtrasState)
is TonMemo -> handleTonMemo(action, sendState, sendState.transactionExtrasState)
is CosmosMemo -> handleCosmosMemo(action, sendState, sendState.transactionExtrasState)
is HederaMemo -> handleHederaMemo(action, sendState, sendState.transactionExtrasState)
is AlgorandMemo -> handleAlgorandMemo(action, sendState, sendState.transactionExtrasState)
else -> sendState
}
}
@ -52,6 +54,10 @@ class TransactionExtrasReducer : SendInternalReducer {
Blockchain.TerraV1,
Blockchain.TerraV2,
-> TransactionExtrasState(cosmosMemoState = CosmosMemoState())
Blockchain.Hedera, Blockchain.HederaTestnet -> TransactionExtrasState(hederaMemoState = HederaMemoState())
Blockchain.Algorand, Blockchain.AlgorandTestnet -> TransactionExtrasState(
algorandMemoState = AlgorandMemoState(),
)
else -> emptyResult
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
@ -162,4 +168,34 @@ class TransactionExtrasReducer : SendInternalReducer {
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleHederaMemo(
action: HederaMemo,
sendState: SendState,
infoState: TransactionExtrasState,
): SendState {
val result = when (action) {
is HederaMemo.HandleUserInput -> {
val memo = action.data
val input = InputViewValue(memo, true)
infoState.copy(hederaMemoState = HederaMemoState(input, memo))
}
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleAlgorandMemo(
action: AlgorandMemo,
sendState: SendState,
infoState: TransactionExtrasState,
): SendState {
val result = when (action) {
is AlgorandMemo.HandleUserInput -> {
val memo = action.data
val input = InputViewValue(memo, true)
infoState.copy(algorandMemoState = AlgorandMemoState(input, memo))
}
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
}

View file

@ -23,18 +23,21 @@ data class AddressState(
fun isReady(): Boolean = error == null && destinationWalletAddress?.isNotEmpty() ?: false
}
@Deprecated("Legacy")
data class TransactionExtrasState(
val xlmMemo: XlmMemoState? = null,
val binanceMemo: BinanceMemoState? = null,
val xrpDestinationTag: XrpDestinationTagState? = null,
val tonMemoState: TonMemoState? = null,
val cosmosMemoState: CosmosMemoState? = null,
val hederaMemoState: HederaMemoState? = null,
val algorandMemoState: AlgorandMemoState? = null,
) : IdStateHolder {
override val stateId: StateId = StateId.TRANSACTION_EXTRAS
fun isNull(): Boolean {
return xlmMemo == null && binanceMemo == null && xrpDestinationTag == null && tonMemoState == null &&
cosmosMemoState == null
cosmosMemoState == null && hederaMemoState == null && algorandMemoState == null
}
fun isEmpty(): Boolean {
@ -43,8 +46,16 @@ data class TransactionExtrasState(
val isXrpEmpty = xrpDestinationTag?.viewFieldValue?.value?.isEmpty() ?: false
val isTonEmpty = tonMemoState?.viewFieldValue?.value?.isEmpty() ?: false
val isCosmosEmpty = cosmosMemoState?.viewFieldValue?.value?.isEmpty() ?: false
val isHederaEmpty = hederaMemoState?.viewFieldValue?.value?.isEmpty() ?: false
val isAlgorandEmpty = algorandMemoState?.viewFieldValue?.value?.isEmpty() ?: false
return isXlmEmpty || isBinanceEmpty || isXrpEmpty || isTonEmpty || isCosmosEmpty
return isXlmEmpty ||
isBinanceEmpty ||
isXrpEmpty ||
isTonEmpty ||
isCosmosEmpty ||
isHederaEmpty ||
isAlgorandEmpty
}
}
@ -120,6 +131,18 @@ data class CosmosMemoState(
val error: TransactionExtraError? = null,
)
data class HederaMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: String? = null,
val error: TransactionExtraError? = null,
)
data class AlgorandMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: String? = null,
val error: TransactionExtraError? = null,
)
enum class TransactionExtraError {
INVALID_DESTINATION_TAG,
INVALID_XLM_MEMO,

View file

@ -234,6 +234,24 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
.onEach { store.dispatch(TransactionExtrasAction.CosmosMemo.HandleUserInput(it)) }
.launchIn(mainScope)
etHederaMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter {
val info = store.state.sendState.transactionExtrasState
info.hederaMemoState?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.HederaMemo.HandleUserInput(it)) }
.launchIn(mainScope)
etAlgorandMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter {
val info = store.state.sendState.transactionExtrasState
info.algorandMemoState?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.AlgorandMemo.HandleUserInput(it)) }
.launchIn(mainScope)
}
private fun onCodeScanned(scannedCode: String) {

View file

@ -62,7 +62,8 @@ internal class SendStateSubscriber(
}
}
@Suppress("ComplexMethod")
@Deprecated("Legacy")
@Suppress("ComplexMethod", "LongMethod")
private fun handleTransactionExtrasState(fg: SendFragment, infoState: TransactionExtrasState) =
with(fg.binding.lSendAddress) {
fun showView(view: View, info: Any?) {
@ -73,6 +74,8 @@ internal class SendStateSubscriber(
showView(binanceMemoContainer, infoState.binanceMemo)
showView(tonMemoContainer, infoState.tonMemoState)
showView(cosmosMemoContainer, infoState.cosmosMemoState)
showView(hederaMemoContainer, infoState.hederaMemoState)
showView(algorandMemoContainer, infoState.algorandMemoState)
infoState.xlmMemo?.let {
if (!it.viewFieldValue.isFromUserInput) etXlmMemo.setText(it.viewFieldValue.value)
@ -129,6 +132,26 @@ internal class SendStateSubscriber(
etCosmosMemo.setText(it.viewFieldValue.value)
}
}
infoState.hederaMemoState?.let {
if (infoState.hederaMemoState.error != null) {
tilHederaMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
} else {
tilHederaMemo.error = null
}
if (!it.viewFieldValue.isFromUserInput) {
etHederaMemo.setText(it.viewFieldValue.value)
}
}
infoState.algorandMemoState?.let {
if (infoState.algorandMemoState.error != null) {
tilAlgorandMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
} else {
tilAlgorandMemo.error = null
}
if (!it.viewFieldValue.isFromUserInput) {
etAlgorandMemo.setText(it.viewFieldValue.value)
}
}
}
@Suppress("ComplexMethod")

View file

@ -83,7 +83,9 @@ internal fun BriefNetworkItem(model: NetworkItemState, modifier: Modifier = Modi
Icon(
painter = painterResource(id = model.iconResId.value),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens.size20),
modifier = Modifier
.size(size = TangemTheme.dimens.size20)
.clip(CircleShape),
tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary,
)

View file

@ -80,6 +80,13 @@ object TokenListPreviewData {
iconResId = mutableStateOf(R.drawable.ic_bsc_16),
isMainNetwork = false,
),
// check icon clipping
NetworkItemState.ReadContent(
name = "SHIBARIUM",
protocolName = "BEP20",
iconResId = mutableStateOf(R.drawable.ic_shibarium_22),
isMainNetwork = false,
),
)
}
}

View file

@ -3,14 +3,20 @@ package com.tangem.tap.features.tokens.impl.presentation.viewmodels
import arrow.core.Either
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.core.navigation.NavigationAction
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import timber.log.Timber
import kotlin.properties.Delegates
@ -24,6 +30,8 @@ import kotlin.properties.Delegates
internal class TokensListMigration(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
) {
private var currentNewCoins: List<CryptoCurrency.Coin> by Delegates.notNull()
@ -76,31 +84,72 @@ internal class TokensListMigration(
}
}
fun onSaveButtonClick(
suspend fun onSaveButtonClick(
changedTokensList: MutableList<TokenWithBlockchain>,
changedBlockchainList: List<Blockchain>,
) {
store.dispatch(
action = TokensAction.SaveChanges(
currentTokens = currentNewTokens,
currentCoins = currentNewCoins,
changedTokens = changedTokensList.mapNotNull {
cryptoCurrencyFactory.createToken(
sdkToken = it.token,
blockchain = it.blockchain,
extraDerivationPath = null,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
},
changedCoins = changedBlockchainList.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = it,
extraDerivationPath = null,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
},
userWallet = currentUserWallet,
),
val changedTokens = changedTokensList.mapNotNull {
cryptoCurrencyFactory.createToken(
sdkToken = it.token,
blockchain = it.blockchain,
extraDerivationPath = null,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
}
val changedCoins = changedBlockchainList.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = it,
extraDerivationPath = null,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
}
val blockchainsToAdd = changedCoins.filterNot(currentNewCoins::contains)
val blockchainsToRemove = currentNewCoins.filterNot(changedCoins::contains)
val tokensToAdd = changedTokens.filterNot(currentNewTokens::contains)
val tokensToRemove = currentNewTokens.filterNot { token -> changedTokens.any { it == token } }
removeCurrenciesIfNeeded(
userWalletId = currentUserWallet.walletId,
currencies = blockchainsToRemove + tokensToRemove,
)
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
store.dispatchDebugErrorNotification(message = "Nothing to save")
store.dispatchOnMain(NavigationAction.PopBackTo())
return
}
val currencyList = blockchainsToAdd + tokensToAdd
derivePublicKeysUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList)
.onRight {
addCryptoCurrenciesUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
.onLeft { Timber.e("Failed to derive public keys: $it") }
}
private suspend fun removeCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) return
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies)
walletManagersFacade.remove(
userWalletId = userWalletId,
networks = currencies
.filterIsInstance<CryptoCurrency.Coin>()
.mapTo(hashSetOf(), CryptoCurrency::network),
)
walletManagersFacade.removeTokens(
userWalletId = userWalletId,
tokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
)
}
}

View file

@ -13,12 +13,14 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.getGreyedOutIconRes
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.extensions.canHandleBlockchain
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.supportedTokens
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
@ -67,6 +69,8 @@ internal class TokensListViewModel @Inject constructor(
private val router: TokensListRouter,
private val dispatchers: AppCoroutineDispatcherProvider,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
derivePublicKeysUseCase: DerivePublicKeysUseCase,
addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
analyticsEventHandler: AnalyticsEventHandler,
getCurrenciesUseCase: GetCryptoCurrenciesUseCase,
) : ViewModel(), DefaultLifecycleObserver {
@ -88,6 +92,8 @@ internal class TokensListViewModel @Inject constructor(
private val tokensListMigration = TokensListMigration(
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
getCurrenciesUseCase = getCurrenciesUseCase,
derivePublicKeysUseCase = derivePublicKeysUseCase,
addCryptoCurrenciesUseCase = addCryptoCurrenciesUseCase,
)
init {
@ -303,10 +309,13 @@ internal class TokensListViewModel @Inject constructor(
fun onSaveButtonClick() {
analyticsSender.sendWhenSaveButtonClicked()
tokensListMigration.onSaveButtonClick(
changedTokensList = changedTokensList,
changedBlockchainList = changedBlockchainList,
)
viewModelScope.launch(dispatchers.main) {
tokensListMigration.onSaveButtonClick(
changedTokensList = changedTokensList,
changedBlockchainList = changedBlockchainList,
)
}
}
private fun onSearchValueChange(newValue: String) {

View file

@ -1,250 +0,0 @@
package com.tangem.tap.features.tokens.legacy.redux
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.core.navigation.NavigationAction
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.supportsHdWallet
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.card.DefaultDerivePublicKeysUseCase
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
@Suppress("LargeClass")
object TokensMiddleware {
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
val tokensMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
when (action) {
is TokensAction.SaveChanges -> handleSaveChanges(action)
}
next(action)
}
}
}
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
scope.launch {
val scanResponse = action.userWallet.scanResponse
val currentTokens = action.currentTokens
val currentBlockchains = action.currentCoins
val blockchainsToAdd = action.changedCoins.filterNot(currentBlockchains::contains)
val blockchainsToRemove = currentBlockchains.filterNot(action.changedCoins::contains)
val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains)
val tokensToRemove = currentTokens.filterNot { token -> action.changedTokens.any { it == token } }
removeCurrenciesIfNeeded(
userWalletId = action.userWallet.walletId,
currencies = blockchainsToRemove + tokensToRemove,
)
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
store.dispatchDebugErrorNotification(message = "Nothing to save")
store.dispatchOnMain(NavigationAction.PopBackTo())
return@launch
}
val currencyList = blockchainsToAdd + tokensToAdd
val featureToggles = store.state.daggerGraphState.get(DaggerGraphState::testerFeatureToggles)
if (featureToggles.isDerivePublicKeysRefactoringEnabled) {
val derivePublicKeys = DefaultDerivePublicKeysUseCase(
tangemSdkManager = tangemSdkManager,
derivationsRepository = store.state.daggerGraphState.get(DaggerGraphState::derivationsRepository),
)
derivePublicKeys(userWalletId = action.userWallet.walletId, currencies = currencyList)
.onRight {
addCryptoCurrenciesUseCase(
userWalletId = action.userWallet.walletId,
currencies = currencyList,
)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
.onLeft { Timber.e("Failed to derive public keys: $it") }
} else {
// TODO: delete [REDACTED_JIRA]
if (scanResponse.supportsHdWallet()) {
deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) {
submitAdd(
userWallet = action.userWallet,
updatedScanResponse = it,
currencyList = currencyList,
)
}
} else {
submitAdd(action.userWallet, scanResponse, currencyList)
}
}
}
}
@Deprecated(message = "Use DerivePublicKeysUseCase instead")
private fun deriveMissingCoins(
scanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
onSuccess: (ScanResponse) -> Unit,
) {
val config = CardConfig.createConfig(scanResponse.card)
val derivationDataList = currencyList.mapNotNull { currency ->
val curve = config.primaryCurve(blockchain = Blockchain.fromId(currency.network.id.value))
curve?.let { getDerivations(curve, scanResponse, currency) }
}
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
derivationDataList.forEach {
val current = this[it.derivations.first]
if (current != null) {
current.addAll(it.derivations.second)
current.distinct()
} else {
this[it.derivations.first] = it.derivations.second.toMutableList()
}
}
}
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
scope.launch {
val result = tangemSdkManager.derivePublicKeys(
cardId = null,
derivations = derivations,
)
when (result) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data.entries
val oldDerivedKeys = scanResponse.derivedKeys
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys)
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
}
}
}
}
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: CryptoCurrency,
): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val blockchain = Blockchain.fromId(currency.network.id.value)
val supportedCurves = blockchain.getSupportedCurves()
val path = blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
.takeIf { supportedCurves.contains(curve) }
val customPath = currency.network.derivationPath.value?.let {
DerivationPath(it)
}.takeIf { supportedCurves.contains(curve) }
val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
if (currency is CryptoCurrency.Coin && blockchain == Blockchain.Cardano) {
currency.network.derivationPath.value?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
class DerivationData(val derivations: Pair<ByteArrayKey, List<DerivationPath>>)
private fun submitAdd(
userWallet: UserWallet,
updatedScanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
) {
scope.launch {
userWalletsListManager.update(
userWalletId = userWallet.walletId,
update = { it.copy(scanResponse = updatedScanResponse) },
).doOnSuccess {
addCryptoCurrenciesUseCase(userWallet.walletId, currencyList)
}
}
store.dispatchOnMain(NavigationAction.PopBackTo())
}
private suspend fun removeCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) return
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies)
walletManagersFacade.remove(
userWalletId = userWalletId,
networks = currencies
.filterIsInstance<CryptoCurrency.Coin>()
.mapTo(hashSetOf(), CryptoCurrency::network),
)
walletManagersFacade.removeTokens(
userWalletId = userWalletId,
tokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
)
}
}

View file

@ -46,6 +46,11 @@ object TradeCryptoMiddleware {
}
}
private val isSendRedesignedEnabled: Boolean
get() = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::sendFeatureToggles,
).isRedesignedSendEnabled
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun handle(state: () -> AppState?, action: TradeCryptoAction) {
if (DemoHelper.tryHandle(state, action)) return
@ -54,11 +59,21 @@ 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.SendToken -> handleSendToken(action = action)
is TradeCryptoAction.SendCoin -> handleSendCoin(action = action)
is TradeCryptoAction.Swap -> openSwap(currency = action.cryptoCurrency)
is TradeCryptoAction.SendToken -> {
if (isSendRedesignedEnabled) {
handleNewSendToken(action = action)
} else {
handleSendToken(action = action)
}
}
is TradeCryptoAction.SendCoin -> {
if (isSendRedesignedEnabled) {
handleNewSendCoin(action = action)
} else {
handleSendCoin(action = action)
}
}
}
}
@ -280,4 +295,35 @@ object TradeCryptoMiddleware {
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
}
}
private fun handleNewSendToken(action: TradeCryptoAction.SendToken) {
handleNewSend(
userWalletId = action.userWallet.walletId.stringValue,
txInfo = action.transactionInfo,
currency = action.tokenCurrency,
)
}
private fun handleNewSendCoin(action: TradeCryptoAction.SendCoin) {
handleNewSend(
userWalletId = action.userWallet.walletId.stringValue,
txInfo = action.transactionInfo,
currency = action.coinStatus.currency,
)
}
private fun handleNewSend(
userWalletId: String,
txInfo: TradeCryptoAction.TransactionInfo?,
currency: CryptoCurrency,
) {
val bundle = bundleOf(
SendRouter.CRYPTO_CURRENCY_KEY to currency,
SendRouter.USER_WALLET_ID_KEY to userWalletId,
SendRouter.TRANSACTION_ID_KEY to txInfo?.transactionId,
SendRouter.DESTINATION_ADDRESS_KEY to txInfo?.destinationAddress,
SendRouter.AMOUNT_KEY to txInfo?.amount,
)
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
}
}

View file

@ -67,6 +67,6 @@ interface ExchangeUrlBuilder {
companion object {
const val SCHEME = "https"
const val SUCCESS_URL = "tangem://success.tangem.com"
const val SUCCESS_URL = "https://tangem.com/success"
}
}

View file

@ -41,4 +41,11 @@ data class MoonPayCurrencies(
val isSupportedInUS: Boolean = false,
val isSellSupported: Boolean = false,
val notAllowedUSStates: List<String> = emptyList(),
val metadata: MoonPayCurrenciesMetadata? = null,
)
@JsonClass(generateAdapter = true)
data class MoonPayCurrenciesMetadata(
val contractAddress: String?,
val networkCode: String?,
)

View file

@ -81,6 +81,7 @@ class MoonPayService(
override fun availableForSell(currency: Currency): Boolean {
val availableForSell = status?.availableForSell ?: return false
val metadata = status?.responseCurrencies?.map { it.metadata }
if (!isSellAllowed()) return false
return when (currency) {
@ -92,7 +93,9 @@ class MoonPayService(
else -> availableForSell.contains(currency.currencySymbol)
}
}
is Currency.Token -> false
is Currency.Token -> {
metadata?.any { it?.contractAddress.equals(currency.token.contractAddress, ignoreCase = true) } ?: false
}
}
}

View file

@ -1,298 +0,0 @@
package com.tangem.tap.proxy
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.Currency.NonNativeToken
import com.tangem.lib.crypto.models.errors.UserCancelledException
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.scope
import com.tangem.tap.userWalletsListManager
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.coroutines.suspendCoroutine
import com.tangem.tap.domain.model.Currency as WalletModelCurrency
class DerivationManagerImpl(
private val appStateHolder: AppStateHolder,
private val currenciesRepository: CurrenciesRepository,
private val networksRepository: NetworksRepository,
) : DerivationManager {
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
override suspend fun deriveAndAddTokens(currency: Currency) = suspendCoroutine { continuation ->
val selectedUserWallet = requireNotNull(
userWalletsListManager.selectedUserWalletSync,
) { "selectedUserWallet shouldn't be null" }
val scanResponse = selectedUserWallet.scanResponse
val blockchain = requireNotNull(
Blockchain.fromNetworkId(currency.networkId),
) { "unsupported blockchain" }
val derivationStyleProvider = scanResponse.derivationStyleProvider
val derivationPath = requireNotNull(
blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath,
) { "derivationPath shouldn't be null" }
val hasDerivation = scanResponse.hasDerivation(
blockchain,
derivationPath,
)
if (hasDerivation) {
scope.launch {
addToken(
userWalletId = selectedUserWallet.walletId,
blockchain = blockchain,
currency = currency,
derivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
continuation.resumeWith(Result.success(derivationPath))
}
} else {
val blockchainNetwork = BlockchainNetwork(blockchain, scanResponse.derivationStyleProvider)
val appCurrency = com.tangem.tap.domain.model.Currency.fromBlockchainNetwork(
blockchainNetwork,
getAppToken(currency),
)
deriveMissingBlockchains(
scanResponse = scanResponse,
currencyList = listOf(appCurrency),
onSuccess = { updatedScanResponse ->
scope.launch {
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { it.copy(scanResponse = updatedScanResponse) },
)
addToken(
userWalletId = selectedUserWallet.walletId,
blockchain = blockchain,
currency = currency,
derivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
continuation.resumeWith(Result.success(derivationPath))
}
},
onFailure = {
continuation.resumeWith(Result.failure(it))
},
)
}
}
private suspend fun addToken(
userWalletId: UserWalletId,
blockchain: Blockchain,
currency: Currency,
derivationPath: String,
derivationStyleProvider: DerivationStyleProvider,
) {
val cryptoCurrency = convertCurrency(
blockchain = blockchain,
currency = currency,
derivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
addCryptoCurrenciesUseCase(userWalletId, cryptoCurrency)
}
private fun convertCurrency(
blockchain: Blockchain,
currency: Currency,
derivationPath: String,
derivationStyleProvider: DerivationStyleProvider,
): CryptoCurrency {
val cryptoCurrencyFactory = CryptoCurrencyFactory()
return when (currency) {
is Currency.NativeToken -> {
cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
}
is NonNativeToken -> {
val sdkToken = Token(
name = currency.name,
symbol = currency.symbol,
contractAddress = currency.contractAddress,
decimals = currency.decimalCount,
id = currency.id,
)
cryptoCurrencyFactory.createToken(
sdkToken = sdkToken,
blockchain = blockchain,
extraDerivationPath = derivationPath,
derivationStyleProvider = derivationStyleProvider,
)
}
} as CryptoCurrency
}
private fun deriveMissingBlockchains(
scanResponse: ScanResponse,
currencyList: List<WalletModelCurrency>,
onSuccess: (ScanResponse) -> Unit,
onFailure: (Exception) -> Unit,
) {
val config = CardConfig.createConfig(scanResponse.card)
val derivationDataList = currencyList.mapNotNull { currency ->
val curve = config.primaryCurve(currency.blockchain)
curve?.let { getDerivations(curve, scanResponse, currency) }
}
val derivations = buildMap<ByteArrayKey, MutableList<DerivationPath>> {
derivationDataList.forEach {
val current = this[it.derivations.first]
if (current != null) {
current.addAll(it.derivations.second)
current.distinct()
} else {
this[it.derivations.first] = it.derivations.second.toMutableList()
}
}
}
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
scope.launch {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
val result = appStateHolder.tangemSdkManager?.derivePublicKeys(
cardId = null, // always ignore cardId in derive task
derivations = derivations,
)
when (result) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data.entries
val oldDerivedKeys = scanResponse.derivedKeys
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
val updatedScanResponse = scanResponse.copy(
derivedKeys = updatedDerivedKeys,
)
if (selectedUserWallet != null) {
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { it.copy(scanResponse = updatedScanResponse) },
)
}
appStateHolder.mainStore?.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
appStateHolder.mainStore?.dispatchDebugErrorNotification(
TapError.CustomError(
"Error derivation",
),
)
onFailure.invoke(handleTangemError(result.error))
}
else -> {
error("result result is null")
}
}
}
}
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currency: com.tangem.tap.domain.model.Currency,
): TokensMiddleware.DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val supportedCurves = currency.blockchain.getSupportedCurves()
val path = currency.blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
.takeIf { supportedCurves.contains(curve) }
val customPath = currency.derivationPath?.let {
DerivationPath(it)
}.takeIf { supportedCurves.contains(curve) }
val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
if (currency is WalletModelCurrency.Blockchain &&
currency.blockchain == Blockchain.Cardano
) {
currency.derivationPath?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
private fun getAppToken(currency: Currency): Token? {
return if (currency is NonNativeToken) {
Token(
symbol = currency.symbol,
contractAddress = currency.contractAddress,
decimals = currency.decimalCount,
)
} else {
null
}
}
/**
* Simple error handler
* for now specifically handle only UserCancelled
*
* @param error [TangemError]
*/
private fun handleTangemError(error: TangemError): Exception {
if (error is TangemSdkError.UserCancelled) {
return UserCancelledException()
}
return IllegalStateException(error.customMessage)
}
}

View file

@ -4,9 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.tap.proxy.*
@ -56,18 +54,4 @@ internal object ProxyModule {
walletManagersFacade = walletManagersFacade,
)
}
@Provides
@Singleton
fun provideDerivationManager(
appStateHolder: AppStateHolder,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
): DerivationManager {
return DerivationManagerImpl(
appStateHolder = appStateHolder,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
)
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.tap.proxy.redux
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.features.managetokens.navigation.ManageTokensRouter
import com.tangem.features.managetokens.navigation.ManageTokensUi
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
@ -19,7 +19,7 @@ sealed interface DaggerGraphAction : Action {
val walletRouter: WalletRouter,
val walletConnectInteractor: WalletConnectInteractor,
val tokenDetailsRouter: TokenDetailsRouter,
val manageTokensRouter: ManageTokensRouter,
val manageTokensUi: ManageTokensUi,
val cardSdkConfigRepository: CardSdkConfigRepository,
val sendRouter: SendRouter,
val qrScanningRouter: QrScanningRouter,

View file

@ -18,7 +18,7 @@ object DaggerGraphReducer {
walletRouter = action.walletRouter,
walletConnectInteractor = action.walletConnectInteractor,
tokenDetailsRouter = action.tokenDetailsRouter,
manageTokensRouter = action.manageTokensRouter,
manageTokensUi = action.manageTokensUi,
cardSdkConfigRepository = action.cardSdkConfigRepository,
sendRouter = action.sendRouter,
qrScanningRouter = action.qrScanningRouter,

View file

@ -1,5 +1,7 @@
package com.tangem.tap.proxy.redux
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -7,17 +9,15 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
import com.tangem.features.managetokens.navigation.ManageTokensRouter
import com.tangem.features.managetokens.navigation.ManageTokensUi
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.tester.api.TesterFeatureToggles
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.features.wallet.navigation.WalletRouter
@ -39,7 +39,7 @@ data class DaggerGraphState(
val walletConnectInteractor: WalletConnectInteractor? = null,
val tokenDetailsRouter: TokenDetailsRouter? = null,
val manageTokensFeatureToggles: ManageTokensFeatureToggles? = null,
val manageTokensRouter: ManageTokensRouter? = null,
val manageTokensUi: ManageTokensUi? = null,
val scanCardProcessor: ScanCardProcessor? = null,
val cardSdkConfigRepository: CardSdkConfigRepository? = null,
val appCurrencyRepository: AppCurrencyRepository? = null,
@ -52,11 +52,9 @@ data class DaggerGraphState(
val sendFeatureToggles: SendFeatureToggles? = null,
val sendRouter: SendRouter? = null,
val qrScanningRouter: QrScanningRouter? = null,
// FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList
val currenciesRepository: CurrenciesRepository? = null,
val derivationsRepository: DerivationsRepository? = null,
val testerFeatureToggles: TesterFeatureToggles? = null,
val blockchainDataStorage: BlockchainDataStorage? = null,
val accountCreator: AccountCreator? = null,
) : StateType {
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {

View file

@ -260,6 +260,70 @@
</FrameLayout>
<FrameLayout
android:id="@+id/hederaMemoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilHederaMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_extras_hint_memo"
style="@style/SecondaryTextInputLayout"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etHederaMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_secondary"
android:textColor="@color/text_primary_1"
android:ellipsize="end"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="Hedera memo" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/algorandMemoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAlgorandMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_extras_hint_memo"
style="@style/SecondaryTextInputLayout"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAlgorandMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_secondary"
android:textColor="@color/text_primary_1"
android:ellipsize="end"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="Algorand memo" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
</LinearLayout>
</LinearLayout>

View file

@ -125,6 +125,7 @@ sealed class AnalyticsParam {
const val SOURCE = "Source"
const val BALANCE = "Balance"
const val BATCH = "Batch"
const val TYPE = "Type"
const val FEE_TYPE = "Fee Type"
const val PERMISSION_TYPE = "Permission Type"
const val PRODUCT_TYPE = "Product Type"

View file

@ -33,7 +33,6 @@ dependencies {
implementation(deps.timber)
/** Network */
implementation(deps.krateSharedPref)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.okHttp)

View file

@ -7,7 +7,7 @@ import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class BigDecimalAdapter {
internal class BigDecimalAdapter {
@FromJson
fun fromJson(value: String) = BigDecimal(value)

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.api.common
import com.squareup.moshi.*
import org.joda.time.DateTime
internal class DateTimeAdapter : JsonAdapter<DateTime>() {
@FromJson
override fun fromJson(reader: JsonReader): DateTime? {
val dateString = reader.nextString() ?: return null
return DateTime.parse(dateString)
}
@ToJson
override fun toJson(writer: JsonWriter, value: DateTime?) {
if (value != null) {
writer.value(value.toString())
} else {
writer.nullValue()
}
}
}

View file

@ -1,14 +1,10 @@
package com.tangem.datasource.api.common
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
import com.squareup.moshi.*
import org.joda.time.LocalDate
import org.joda.time.format.DateTimeFormat
class LocalDateAdapter : JsonAdapter<LocalDate>() {
internal class LocalDateAdapter : JsonAdapter<LocalDate>() {
private val formatter = DateTimeFormat.forPattern("yyyy-MM-dd")

View file

@ -6,7 +6,7 @@ import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
internal class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() {
class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
if (getRawType(returnType) != Call::class.java) {

View file

@ -75,4 +75,11 @@ interface TangemTechApi {
@GET("promotion")
suspend fun getPromotionInfo(@Query("programName") name: String): ApiResponse<PromotionInfoResponse>
@POST("user-network-account")
suspend fun createUserNetworkAccount(
@Header("card_public_key") cardPublicKey: String,
@Header("card_id") cardId: String,
@Body body: CreateUserNetworkAccountBody,
): ApiResponse<CreateUserNetworkAccountResponse>
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CreateUserNetworkAccountBody(
@Json(name = "networkId") val networkId: String,
@Json(name = "walletPublicKey") val publicWalletKey: String,
)

View file

@ -0,0 +1,17 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CreateUserNetworkAccountResponse(
@Json(name = "status") val status: Boolean,
@Json(name = "data") val data: AccountCreated,
) {
@JsonClass(generateAdapter = true)
data class AccountCreated(
@Json(name = "accountId") val accountId: String,
@Json(name = "publicWalletKey") val publicWalletKey: String,
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.config
import com.tangem.blockchain.common.*
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.config.ConfigManager.Companion.IS_CREATING_TWIN_CARDS_ALLOWED
import com.tangem.datasource.config.ConfigManager.Companion.IS_TOP_UP_ENABLED
import com.tangem.datasource.config.models.Config
@ -105,7 +106,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
sprinklr = configValues.sprinklr,
walletConnectProjectId = configValues.walletConnectProjectId,
tangemComAuthorization = configValues.tangemComAuthorization,
express = configValues.express,
express = if (BuildConfig.ENVIRONMENT == "dev") configValues.devExpress else configValues.express,
)
}
@ -145,6 +146,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
jsonRpc = accessTokens.bitcoin?.jsonRPC,
blockBookRest = accessTokens.bitcoin?.blockBookRest,
),
algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest),
)
}
}

View file

@ -40,6 +40,7 @@ class ConfigValueModel(
val tangemComAuthorization: String?,
val chiaFireAcademyApiKey: String?,
val chiaTangemApiKey: String?,
val devExpress: ExpressModel?,
val express: ExpressModel?,
)
@ -66,6 +67,7 @@ data class GetBlockAccessTokens(
@Json(name = "litecoin") val litecoin: GetBlockToken?,
@Json(name = "dash") val dash: GetBlockToken?,
@Json(name = "bitcoin") val bitcoin: GetBlockToken?,
@Json(name = "algorand") val algorand: GetBlockToken?,
)
@JsonClass(generateAdapter = true)

View file

@ -0,0 +1,22 @@
package com.tangem.datasource.di
import com.tangem.blockchain.common.AccountCreator
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.blockchain.DefaultAccountCreator
import com.tangem.lib.auth.AuthProvider
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 AccountCreatorModule {
@Provides
@Singleton
fun provideAccountCreator(authProvider: AuthProvider, @DevTangemApi tangemTechApi: TangemTechApi): AccountCreator {
return DefaultAccountCreator(authProvider, tangemTechApi)
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.AppPreferenceStorage
import com.tangem.datasource.local.AppPreferenceStorageImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AppPreferenceStorageModule {
@Binds
@Singleton
fun bindAppPreferenceStorage(appPreferenceStorageImpl: AppPreferenceStorageImpl): AppPreferenceStorage
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.local.blockchain.DefaultBlockchainDataStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
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 BlockchainDataStorageModule {
@Provides
@Singleton
fun provideBlockchainDataStorage(appPreferencesStore: AppPreferencesStore): BlockchainDataStorage {
return DefaultBlockchainDataStorage(appPreferencesStore)
}
}

View file

@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.BigDecimalAdapter
import com.tangem.datasource.api.common.DateTimeAdapter
import com.tangem.datasource.api.common.LocalDateAdapter
import dagger.Module
import dagger.Provides
@ -22,6 +23,7 @@ class MoshiModule {
return Moshi.Builder()
.add(BigDecimalAdapter())
.add(LocalDateAdapter())
.add(DateTimeAdapter())
.add(KotlinJsonAdapterFactory())
.build()
}

View file

@ -1,13 +0,0 @@
package com.tangem.datasource.local
/**
* Application local storage
*
[REDACTED_AUTHOR]
*/
@Deprecated(message = "Use AppPreferencesStore", level = DeprecationLevel.WARNING)
interface AppPreferenceStorage {
/** Json config with feature toggles 'ToggleName: String - Availability: Boolean' */
var featureToggles: String
}

View file

@ -1,24 +0,0 @@
package com.tangem.datasource.local
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import hu.autsoft.krate.SimpleKrate
import hu.autsoft.krate.default.withDefault
import hu.autsoft.krate.stringPref
import javax.inject.Inject
import javax.inject.Singleton
/**
* Implementation of application local storage
*
* @param context application context
*
[REDACTED_AUTHOR]
*/
@Singleton
internal class AppPreferenceStorageImpl @Inject constructor(
@ApplicationContext context: Context,
) : SimpleKrate(context = context), AppPreferenceStorage {
override var featureToggles: String by stringPref().withDefault("")
}

View file

@ -0,0 +1,31 @@
package com.tangem.datasource.local.blockchain
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CreateUserNetworkAccountBody
import com.tangem.lib.auth.AuthProvider
internal class DefaultAccountCreator(
private val authProvider: AuthProvider,
private val tangemTechApi: TangemTechApi,
) : AccountCreator {
override suspend fun createAccount(blockchain: Blockchain, walletPublicKey: ByteArray): Result<String> {
val request = CreateUserNetworkAccountBody(blockchain.id.removeSuffix("/test"), walletPublicKey.toHexString())
return try {
val response = tangemTechApi.createUserNetworkAccount(
cardPublicKey = authProvider.getCardPublicKey(),
cardId = authProvider.getCardId(),
body = request,
).getOrThrow()
Result.Success(response.data.accountId)
} catch (e: Exception) {
Result.Failure(BlockchainSdkError.FailedToCreateAccount)
}
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.local.blockchain
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
/**
* [BlockchainDataStorage] implementation
*
* @property appPreferencesStore app preferences store
*
[REDACTED_AUTHOR]
*/
internal class DefaultBlockchainDataStorage(
private val appPreferencesStore: AppPreferencesStore,
) : BlockchainDataStorage {
override suspend fun getOrNull(key: String): String? {
return appPreferencesStore.getSyncOrNull(key = stringPreferencesKey(name = key))
}
override suspend fun store(key: String, value: String) {
appPreferencesStore.edit {
it[stringPreferencesKey(key)] = value
}
}
}

View file

@ -52,6 +52,8 @@ object PreferencesKeys {
val IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY by lazy {
booleanPreferencesKey(name = "isTokenSwapPromoChangellyShown")
}
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
}
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.local.txhistory
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
import com.tangem.domain.txhistory.models.Page
import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.utils.extensions.addOrReplace
@ -13,28 +14,16 @@ internal class DefaultTxHistoryItemsStore(
override fun provideStringKey(key: TxHistoryItemsStore.Key): String = key.toString()
override suspend fun getNextPageSyncOrNull(key: TxHistoryItemsStore.Key): Int? {
val storedValue = getSyncOrNull(key) ?: return null
val lastWrappedItems = storedValue.maxBy(PaginationWrapper<*>::page)
val lastPage = lastWrappedItems.page
return if (lastPage <= lastWrappedItems.totalPages) {
lastPage
} else {
null
}
}
override suspend fun getSyncOrNull(key: TxHistoryItemsStore.Key, page: Int): PaginationWrapper<TxHistoryItem>? {
override suspend fun getSyncOrNull(key: TxHistoryItemsStore.Key, page: Page): PaginationWrapper<TxHistoryItem>? {
val storedValue = getSyncOrNull(key)
return storedValue?.firstOrNull { it.page == page }
return storedValue?.firstOrNull { it.currentPage == page }
}
override suspend fun store(key: TxHistoryItemsStore.Key, value: PaginationWrapper<TxHistoryItem>) {
val oldValue = getSyncOrNull(key).orEmpty()
val newValue = oldValue.addOrReplace(value) {
it.page == value.page
it.currentPage == value.currentPage
}
store(key, newValue)

View file

@ -1,15 +1,14 @@
package com.tangem.datasource.local.txhistory
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.txhistory.models.Page
import com.tangem.domain.txhistory.models.PaginationWrapper
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.wallets.models.UserWalletId
interface TxHistoryItemsStore {
suspend fun getNextPageSyncOrNull(key: Key): Int?
suspend fun getSyncOrNull(key: Key, page: Int): PaginationWrapper<TxHistoryItem>?
suspend fun getSyncOrNull(key: Key, page: Page): PaginationWrapper<TxHistoryItem>?
suspend fun remove(key: Key)

View file

@ -4,7 +4,7 @@ import com.tangem.core.deeplink.DeepLink
class BuyCurrencyDeepLink(val onReceive: () -> Unit) : DeepLink {
override val uri: String = "tangem://success.tangem.com"
override val uri: String = "tangem://redirect?action=dismissBrowser"
override fun onReceive(params: Map<String, String>) {
onReceive()

View file

@ -7,14 +7,22 @@ plugins {
}
dependencies {
implementation(project(":core:datasource"))
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Local storages */
implementation(deps.androidx.datastore)
/** Other libraries */
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
/** Core modules */
implementation(projects.core.datasource)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)
testImplementation(deps.test.mockk)

View file

@ -14,13 +14,5 @@
{
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
"version": "undefined"
},
{
"name": "WALLETS_SCROLLING_PREVIEW_ENABLED",
"version": "5.5.0"
},
{
"name": "DERIVE_PUBLIC_KEYS_REFACTORING_ENABLED",
"version": "5.5.0"
}
]

View file

@ -11,7 +11,7 @@ import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage
import com.tangem.core.featuretoggle.version.DefaultVersionProvider
import com.tangem.core.featuretoggles.BuildConfig
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.local.AppPreferenceStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -29,7 +29,7 @@ internal object FeatureTogglesManagerModule {
fun provideFeatureTogglesManager(
@ApplicationContext context: Context,
assetReader: AssetReader,
appPreferenceStorage: AppPreferenceStorage,
appPreferencesStore: AppPreferencesStore,
): FeatureTogglesManager {
val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
val localFeatureTogglesStorage = LocalFeatureTogglesStorage(
@ -41,8 +41,7 @@ internal object FeatureTogglesManagerModule {
return if (BuildConfig.TESTER_MENU_ENABLED) {
DevFeatureTogglesManager(
localFeatureTogglesStorage = localFeatureTogglesStorage,
appPreferenceStorage = appPreferenceStorage,
jsonAdapter = moshi.adapter(),
appPreferencesStore = appPreferencesStore,
versionProvider = versionProvider,
)
} else {

View file

@ -1,25 +1,25 @@
package com.tangem.core.featuretoggle.manager
import androidx.annotation.VisibleForTesting
import com.squareup.moshi.JsonAdapter
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
import com.tangem.core.featuretoggle.utils.associateToggles
import com.tangem.core.featuretoggle.version.VersionProvider
import com.tangem.datasource.local.AppPreferenceStorage
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
import kotlin.properties.Delegates
/**
* Feature toggles manager implementation in DEV build
*
* @property localFeatureTogglesStorage local feature toggles storage
* @property appPreferenceStorage application local storage
* @property jsonAdapter adapter for parsing json
* @property appPreferencesStore application local store
* @property versionProvider application version provider
*/
internal class DevFeatureTogglesManager(
private val localFeatureTogglesStorage: FeatureTogglesStorage,
private val appPreferenceStorage: AppPreferenceStorage,
private val jsonAdapter: JsonAdapter<Map<String, Boolean>>,
private val appPreferencesStore: AppPreferencesStore,
private val versionProvider: VersionProvider,
) : MutableFeatureTogglesManager {
@ -28,11 +28,9 @@ internal class DevFeatureTogglesManager(
override suspend fun init() {
localFeatureTogglesStorage.init()
val savedFeatureToggles = if (appPreferenceStorage.featureToggles.isNotEmpty()) {
jsonAdapter.fromJson(appPreferenceStorage.featureToggles).orEmpty()
} else {
emptyMap()
}
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
) ?: emptyMap()
featureTogglesMap = localFeatureTogglesStorage.featureToggles
.associateToggles(currentVersion = versionProvider.get().orEmpty())
@ -46,10 +44,10 @@ internal class DevFeatureTogglesManager(
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
override fun changeToggle(name: String, isEnabled: Boolean) {
override suspend fun changeToggle(name: String, isEnabled: Boolean) {
featureTogglesMap[name] ?: return
featureTogglesMap[name] = isEnabled
appPreferenceStorage.featureToggles = jsonAdapter.toJson(featureTogglesMap)
appPreferencesStore.storeObject(PreferencesKeys.FEATURE_TOGGLES_KEY, featureTogglesMap)
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)

View file

@ -11,5 +11,5 @@ interface MutableFeatureTogglesManager : FeatureTogglesManager {
fun getFeatureToggles(): Map<String, Boolean>
/** Change availability [isEnabled] of toggle with name [name] */
fun changeToggle(name: String, isEnabled: Boolean)
suspend fun changeToggle(name: String, isEnabled: Boolean)
}

View file

@ -1,5 +1,8 @@
package com.tangem.core.featuretoggle.storage
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Data model with information about feature toggle
*
@ -10,4 +13,8 @@ package com.tangem.core.featuretoggle.storage
*
[REDACTED_AUTHOR]
*/
internal data class FeatureToggle(val name: String, val version: String)
@JsonClass(generateAdapter = true)
internal data class FeatureToggle(
@Json(name = "name") val name: String,
@Json(name = "version") val version: String,
)

View file

@ -2,48 +2,42 @@ package com.tangem.core.featuretoggle.manager
import android.annotation.SuppressLint
import com.google.common.truth.Truth
import com.squareup.moshi.JsonAdapter
import com.tangem.core.featuretoggle.storage.FeatureToggle
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
import com.tangem.core.featuretoggle.utils.associateToggles
import com.tangem.core.featuretoggle.version.VersionProvider
import com.tangem.datasource.local.AppPreferenceStorage
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerifyOrder
import io.mockk.just
import io.mockk.mockk
import io.mockk.verifyAll
import io.mockk.verifyOrder
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.getSyncOrNull
import io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.collections.set
/**
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalCoroutinesApi::class)
@SuppressLint("CheckResult")
internal class DevFeatureTogglesManagerTest {
private val localFeatureTogglesStorage = mockk<FeatureTogglesStorage>()
private val appPreferenceStorage = mockk<AppPreferenceStorage>(relaxed = true)
private val jsonAdapter = mockk<JsonAdapter<Map<String, Boolean>>>()
private val appPreferenceStore = mockk<AppPreferencesStore>(relaxed = true)
private val versionProvider = mockk<VersionProvider>()
private val manager = DevFeatureTogglesManager(
localFeatureTogglesStorage = localFeatureTogglesStorage,
appPreferenceStorage = appPreferenceStorage,
jsonAdapter = jsonAdapter,
appPreferencesStore = appPreferenceStore,
versionProvider = versionProvider,
)
@Test
fun `successfully initialize storage if shared prefs kept feature toggles`() = runTest {
val currentVersion = "1.0.0"
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { appPreferenceStorage.featureToggles } returns savedFeatureToggles
coEvery { jsonAdapter.fromJson(savedFeatureToggles) } returns savedFeatureTogglesMap
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns savedFeatureTogglesMap
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
@ -51,7 +45,6 @@ internal class DevFeatureTogglesManagerTest {
coVerifyOrder {
localFeatureTogglesStorage.init()
jsonAdapter.fromJson(savedFeatureToggles)
versionProvider.get()
}
@ -66,12 +59,12 @@ internal class DevFeatureTogglesManagerTest {
@Test
fun `successfully initialize storage if shared prefs kept empty list`() = runTest {
val currentVersion = "1.0.0"
val sharedPrefFeatureToggles = "[]"
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { appPreferenceStorage.featureToggles } returns sharedPrefFeatureToggles
coEvery { jsonAdapter.fromJson(sharedPrefFeatureToggles) } returns savedFeatureTogglesMap
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns emptyMap()
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
@ -79,7 +72,6 @@ internal class DevFeatureTogglesManagerTest {
coVerifyOrder {
localFeatureTogglesStorage.init()
jsonAdapter.fromJson(sharedPrefFeatureToggles)
versionProvider.get()
}
@ -94,10 +86,12 @@ internal class DevFeatureTogglesManagerTest {
@Test
fun `successfully initialize storage if shared prefs didn't keep feature toggles`() = runTest {
val currentVersion = "1.0.0"
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { appPreferenceStorage.featureToggles } returns ""
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns null
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
@ -107,7 +101,6 @@ internal class DevFeatureTogglesManagerTest {
localFeatureTogglesStorage.init()
versionProvider.get()
}
verifyAll(inverse = true) { jsonAdapter.fromJson(any<String>()) }
val expected = localFeatureToggles
.associateToggles(currentVersion)
@ -119,8 +112,7 @@ internal class DevFeatureTogglesManagerTest {
@Test
fun `successfully initialize storage if versionProvider returns null`() = runTest {
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { appPreferenceStorage.featureToggles } returns savedFeatureToggles
coEvery { jsonAdapter.fromJson(savedFeatureToggles) } returns savedFeatureTogglesMap
coEvery { appPreferenceStore.getSyncOrNull(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns savedFeatureToggles
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns null
@ -128,7 +120,6 @@ internal class DevFeatureTogglesManagerTest {
coVerifyOrder {
localFeatureTogglesStorage.init()
jsonAdapter.fromJson(savedFeatureToggles)
versionProvider.get()
}
@ -180,7 +171,7 @@ internal class DevFeatureTogglesManagerTest {
}
@Test
fun `change toggle that contains in map`() {
fun `change toggle that contains in map`() = runTest {
val changeableToggleName = "INACTIVE_TEST_FEATURE_ENABLED"
val resultMap = mutableMapOf(
changeableToggleName to false,
@ -188,18 +179,16 @@ internal class DevFeatureTogglesManagerTest {
)
manager.setFeatureToggles(resultMap)
coEvery { jsonAdapter.toJson(resultMap) } returns ""
manager.changeToggle(changeableToggleName, true)
resultMap[changeableToggleName] = true
verifyOrder { jsonAdapter.toJson(resultMap) }
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
}
@Test
fun `change toggle that doesn't contains in map`() {
fun `change toggle that doesn't contains in map`() = runTest {
val resultMap = mutableMapOf(
"INACTIVE_TEST_FEATURE_ENABLED" to false,
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
@ -209,8 +198,6 @@ internal class DevFeatureTogglesManagerTest {
manager.changeToggle("FEATURE_TOGGLE", true)
verifyAll(inverse = true) { jsonAdapter.toJson(any()) }
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
}

View file

@ -91,7 +91,8 @@
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
<string name="common_explorer">Обозреватель</string>
<string name="common_fee_label">Комиссия</string>
<string name="common_fee_selector_footer">Сетевые комиссии за транзакции используются для поддержки безопасности сети, поощрения валидаторов, выделения ресурсов и определения приоритета транзакции.</string>
<string name="common_fee_selector_footer">Сетевые комиссии это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузка на сеть, объема транзакции и приоритета исполнения. %s</string>
<string name="common_fee_selector_link_description">Подробнее</string>
<string name="common_fee_selector_option_custom">Свое</string>
<string name="common_fee_selector_option_fast">Быстро</string>
<string name="common_fee_selector_option_market">По рынку</string>
@ -449,6 +450,7 @@
<string name="scan_card_settings_button">Сканировать</string>
<string name="scan_card_settings_message">Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку.</string>
<string name="scan_card_settings_title">Приготовьте свою карту</string>
<string name="send_additional_field_already_included">Уже содержится в введенном адресе</string>
<string name="send_amount_label">Сумма</string>
<string name="send_amount_substract">Вычесть из суммы отправки</string>
<string name="send_amount_substract_footer">Сумма к получению %s</string>
@ -535,6 +537,9 @@
<string name="swapping_give_permission">Дать разрешение</string>
<string name="swapping_high_price_impact_description">Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму.</string>
<string name="swapping_insufficient_funds">Недостаточно средств</string>
<string name="swapping_alert_title">Комиссии</string>
<string name="swapping_alert_cex_description">В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя</string>
<string name="swapping_alert_dex_description">В сумму включена комиссия провайдера сервиса.</string>
<string name="swapping_not_enough_funds_for_fee">Недостаточно средств для оплаты комиссии на вашем %1$s кошельке для создания транзакции. Сначала пополните свой %2$s кошелек.</string>
<string name="swapping_pending_transaction_subtitle">Транзакция в процессе…</string>
<string name="swapping_pending_transaction_title">Подождите</string>

View file

@ -89,7 +89,8 @@
<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 transaction fees are used to support network security, incentivize validators, allocate resources, and determine transaction priority.</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_link_description">Read more</string>
<string name="common_fee_selector_option_custom">Custom</string>
<string name="common_fee_selector_option_fast">Fast</string>
<string name="common_fee_selector_option_market">Market</string>
@ -285,7 +286,6 @@
<string name="manage_tokens_network_selector_non_native_info">Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk.</string>
<string name="manage_tokens_network_selector_non_native_subtitle">Not original or primary blockchain the token is hosted</string>
<string name="manage_tokens_network_selector_non_native_title">Non-native networks</string>
<string name="manage_tokens_network_selector_other_subtitle">Blockchain the cryptocurrency was initially created</string>
<string name="manage_tokens_network_selector_other_title">Networks</string>
<string name="manage_tokens_network_selector_title">Choose networks</string>
<string name="manage_tokens_network_selector_wallet">Wallet</string>
@ -448,6 +448,7 @@
<string name="scan_card_settings_button">Scan Card</string>
<string name="scan_card_settings_message">Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet.</string>
<string name="scan_card_settings_title">Get your card ready!</string>
<string name="send_additional_field_already_included">Already included in the entered address</string>
<string name="send_amount_label">Amount</string>
<string name="send_amount_substract">Subtract from send amount</string>
<string name="send_amount_substract_footer">The recipient will receive %s</string>
@ -549,6 +550,9 @@
<string name="swapping_give_permission">Give Permission</string>
<string name="swapping_high_price_impact_description">Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.</string>
<string name="swapping_insufficient_funds">Insufficient funds</string>
<string name="swapping_alert_title">Fees</string>
<string name="swapping_alert_cex_description">The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address.</string>
<string name="swapping_alert_dex_description">The amount includes the service provider\'s fee.</string>
<string name="swapping_not_enough_funds_for_fee">Insufficient funds in your %1$s wallet to cover fees. Top up your %2$s wallet first.</string>
<string name="swapping_pending_transaction_subtitle">Transaction in progress...</string>
<string name="swapping_pending_transaction_title">Waiting</string>

View file

@ -30,6 +30,8 @@ dependencies {
implementation(deps.compose.ui.tooling)
implementation(deps.compose.ui.utils)
implementation(deps.compose.coil)
implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt)
/** Other libraries */
implementation(deps.compose.accompanist.systemUiController)
@ -38,4 +40,5 @@ dependencies {
implementation(deps.kotlin.immutable.collections)
implementation(deps.zxing.qrCore)
implementation(deps.jodatime)
implementation(deps.timber)
}

View file

@ -39,6 +39,11 @@ fun SpacerH16(modifier: Modifier = Modifier) {
SpacerH(16.dp, modifier)
}
@Composable
fun SpacerH18(modifier: Modifier = Modifier) {
SpacerH(18.dp, modifier)
}
@Composable
fun SpacerH24(modifier: Modifier = Modifier) {
SpacerH(24.dp, modifier)

View file

@ -1,17 +1,13 @@
package com.tangem.core.ui.components.atoms
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.res.TangemTheme
/**
@ -21,7 +17,7 @@ import com.tangem.core.ui.res.TangemTheme
fun Hand(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.padding(vertical = TangemTheme.dimens.spacing8)
.height(handComposableComponentHeight)
.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
@ -37,6 +33,11 @@ fun Hand(modifier: Modifier = Modifier) {
}
}
val handComposableComponentHeight: Dp
@Composable
@ReadOnlyComposable
get() = TangemTheme.dimens.size4 + TangemTheme.dimens.spacing16
// region Preview
@Composable
private fun HandSample(modifier: Modifier = Modifier) {

View file

@ -6,4 +6,6 @@ import androidx.compose.runtime.Immutable
* General interface for bottom sheet config model
*/
@Immutable
interface TangemBottomSheetConfigContent
interface TangemBottomSheetConfigContent {
object Empty : TangemBottomSheetConfigContent
}

View file

@ -39,47 +39,57 @@ fun HorizontalActionChips(
}
}
@Preview
// region Preview
@Preview(widthDp = 360)
@Composable
private fun Preview_HorizontalActionChips_Light(
@PreviewParameter(ActionButtonConfigProvider::class) buttons: ImmutableList<ActionButtonConfig>,
@PreviewParameter(ActionButtonConfigProvider::class) buttons: HorizontalActionChips,
) {
TangemTheme(isDark = false) {
HorizontalActionChips(buttons = buttons)
HorizontalActionChips(buttons = buttons.buttons)
}
}
@Preview
@Preview(widthDp = 360)
@Composable
private fun Preview_HorizontalActionChips_Dark(
@PreviewParameter(ActionButtonConfigProvider::class) buttons: ImmutableList<ActionButtonConfig>,
@PreviewParameter(ActionButtonConfigProvider::class) buttons: HorizontalActionChips,
) {
TangemTheme(isDark = true) {
HorizontalActionChips(buttons = buttons)
HorizontalActionChips(buttons = buttons.buttons)
}
}
private class ActionButtonConfigProvider : CollectionPreviewParameterProvider<ActionButtonConfig>(
collection = persistentListOf(
ActionButtonConfig(
text = TextReference.Str(value = "Buy"),
iconResId = R.drawable.ic_plus_24,
onClick = {},
),
ActionButtonConfig(
text = TextReference.Str(value = "Send"),
iconResId = R.drawable.ic_arrow_up_24,
onClick = {},
),
ActionButtonConfig(
text = TextReference.Str(value = "Receive"),
iconResId = R.drawable.ic_arrow_down_24,
onClick = {},
),
ActionButtonConfig(
text = TextReference.Str(value = "Exchange"),
iconResId = R.drawable.ic_exchange_vertical_24,
onClick = {},
private class ActionButtonConfigProvider : CollectionPreviewParameterProvider<HorizontalActionChips>(
collection = listOf(
HorizontalActionChips(
buttons = persistentListOf(
ActionButtonConfig(
text = TextReference.Str(value = "Buy"),
iconResId = R.drawable.ic_plus_24,
onClick = {},
),
ActionButtonConfig(
text = TextReference.Str(value = "Send"),
iconResId = R.drawable.ic_arrow_up_24,
onClick = {},
),
ActionButtonConfig(
text = TextReference.Str(value = "Receive"),
iconResId = R.drawable.ic_arrow_down_24,
onClick = {},
),
ActionButtonConfig(
text = TextReference.Str(value = "Exchange"),
iconResId = R.drawable.ic_exchange_vertical_24,
onClick = {},
),
),
),
),
)
)
private data class HorizontalActionChips(
val buttons: ImmutableList<ActionButtonConfig>,
)
// endregion Preview

View file

@ -1,11 +1,13 @@
package com.tangem.core.ui.components.buttons
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -51,12 +53,22 @@ fun SecondarySmallButton(config: SmallButtonConfig, modifier: Modifier = Modifie
@Composable
private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: Modifier = Modifier) {
val shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)
val backgroundColor by animateColorAsState(
targetValue = if (isPrimary) TangemTheme.colors.button.primary else TangemTheme.colors.button.secondary,
label = "Update background color",
)
val textColor by animateColorAsState(
targetValue = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1,
label = "Update text color",
)
Box(
modifier = modifier
.defaultMinSize(minWidth = TangemTheme.dimens.size46, minHeight = TangemTheme.dimens.size24)
.clip(shape)
.background(
color = if (isPrimary) TangemTheme.colors.button.primary else TangemTheme.colors.button.secondary,
color = backgroundColor,
shape = shape,
)
.clickable(enabled = true, onClick = config.onClick)
@ -66,8 +78,11 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
contentAlignment = Alignment.Center,
) {
Text(
modifier = Modifier.padding(
horizontal = TangemTheme.dimens.spacing10,
),
text = config.text.resolveReference(),
color = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1,
color = textColor,
maxLines = 1,
style = TangemTheme.typography.button,
)
@ -97,7 +112,7 @@ private fun ButtonsSample() {
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
val config = SmallButtonConfig(
text = TextReference.Str(value = "Add"),
text = TextReference.Str(value = "Adddddddd"),
onClick = {},
)
PrimarySmallButton(config = config)

View file

@ -11,7 +11,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
@ -87,7 +86,6 @@ private fun Button(
Row(
modifier = modifier
.heightIn(min = TangemTheme.dimens.size36)
.clip(shape)
.background(color = backgroundColor, shape = shape)
.clickable(enabled = config.enabled, onClick = config.onClick)
.padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing24)

View file

@ -35,6 +35,7 @@ fun TangemButton(
elevation: ButtonElevation = TangemButtonsDefaults.elevation,
textStyle: TextStyle = TangemTheme.typography.button,
shape: Shape = size.toShape(),
iconPadding: Dp = size.toIconPadding(),
) {
val multipleClickPreventer = remember { MultipleClickPreventer.get() }
@ -53,7 +54,7 @@ fun TangemButton(
ButtonContentContainer(
buttonIcon = icon,
iconPadding = size.toIconPadding(),
iconPadding = iconPadding,
showProgress = showProgress,
progressIndicator = {
CircularProgressIndicator(

View file

@ -1,10 +1,16 @@
package com.tangem.core.ui.components.buttons.segmentedbutton
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@ -58,11 +64,16 @@ inline fun <reified T> SegmentedButtons(
val leftRadius = if (index == 0) TangemTheme.dimens.radius26 else TangemTheme.dimens.radius0
val rightRadius = if (index == config.lastIndex) TangemTheme.dimens.radius26 else TangemTheme.dimens.radius0
val animateColor by animateColorAsState(
targetValue = if (index == selected) selectedColor else color,
label = "Segmented Button Selected Color Animation",
animationSpec = spring(stiffness = Spring.StiffnessMedium),
)
Box(
modifier = Modifier
.weight(1f)
.background(
color = if (index == selected) selectedColor else color,
color = animateColor,
shape = RoundedCornerShape(
topStart = leftRadius,
topEnd = rightRadius,

View file

@ -3,6 +3,7 @@ package com.tangem.core.ui.components.fields
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@ -54,6 +55,7 @@ fun AmountTextField(
keyboardOptions: KeyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
keyboardActions: KeyboardActions = KeyboardActions.Default,
) {
val decimalFormat = rememberDecimalFormat()
@ -75,6 +77,7 @@ fun AmountTextField(
textStyle = textStyle,
color = color,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = true,
visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat),
decorationBox = { innerTextField ->

View file

@ -1,7 +1,9 @@
package com.tangem.core.ui.components.fields
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
@ -33,6 +35,7 @@ fun SimpleTextField(
singleLine: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
color: Color = TangemTheme.colors.text.primary1,
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
readOnly: Boolean = false,
@ -42,26 +45,31 @@ fun SimpleTextField(
mutableStateOf(
TextFieldValue(
text = value,
selection = when {
value.isEmpty() -> TextRange.Zero
else -> TextRange(value.length, value.length)
},
selection = getValueRange(value),
),
)
}
val focusRequester = remember { FocusRequester.Default }
val customTextSelectionColors = TextSelectionColors(
handleColor = TangemTheme.colors.text.secondary,
backgroundColor = TangemTheme.colors.text.secondary.copy(alpha = 0.4f),
handleColor = TangemTheme.colors.text.accent,
backgroundColor = TangemTheme.colors.text.accent.copy(alpha = 0.3f),
)
val textFieldValue = textFieldValueState.copy(text = value)
SideEffect {
if (textFieldValue.selection != textFieldValueState.selection ||
textFieldValue.composition != textFieldValueState.composition
) {
textFieldValueState = textFieldValue
val isSelectionChanged by remember {
derivedStateOf {
textFieldValue.selection != textFieldValueState.selection ||
textFieldValue.composition != textFieldValueState.composition ||
textFieldValue.text != textFieldValueState.text
}
}
LaunchedEffect(key1 = isSelectionChanged) {
if (isSelectionChanged) {
textFieldValueState = textFieldValue.copy(
selection = getValueRange(value),
)
}
}
@ -86,20 +94,46 @@ fun SimpleTextField(
readOnly = readOnly,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
decorationBox = decorationBox ?: { textValue ->
Box {
if (value.isBlank() && placeholder != null) {
Text(
text = placeholder.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.disabled,
)
}
textValue()
}
SimpleTextPlaceholder(
placeholder = placeholder,
value = value,
textStyle = textStyle,
textValue = textValue,
)
},
modifier = modifier
.focusRequester(focusRequester),
)
}
}
private fun getValueRange(value: String) = when {
value.isEmpty() -> TextRange.Zero
else -> TextRange(value.length, value.length)
}
@Composable
private fun SimpleTextPlaceholder(
placeholder: TextReference?,
value: String,
textStyle: TextStyle,
textValue: @Composable () -> Unit,
) {
Box {
if (value.isBlank() && placeholder != null) {
AnimatedContent(
targetState = placeholder,
label = "Placeholder Change Animation",
) {
Text(
text = it.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.disabled,
)
}
}
textValue()
}
}

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
@ -51,6 +52,7 @@ fun InputRowEnterAmount(
titleColor: Color = TangemTheme.colors.text.secondary,
textColor: Color = TangemTheme.colors.text.primary1,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
iconRes: Int? = null,
iconTint: Color = TangemTheme.colors.icon.informative,
onIconClick: (() -> Unit)? = null,
@ -79,6 +81,7 @@ fun InputRowEnterAmount(
color = textColor,
textStyle = TangemTheme.typography.body2,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8),
)

View file

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -49,6 +50,7 @@ fun InputRowEnterInfoAmount(
textColor: Color = TangemTheme.colors.text.primary1,
infoColor: Color = TangemTheme.colors.text.tertiary,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
showDivider: Boolean = false,
) {
DividerContainer(
@ -74,6 +76,7 @@ fun InputRowEnterInfoAmount(
color = textColor,
textStyle = TangemTheme.typography.body2,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8)
.weight(1f),

View file

@ -7,6 +7,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterEnd
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -16,6 +17,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.components.inputrow.inner.CrossIcon
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.inputrow.inner.PasteButton
import com.tangem.core.ui.extensions.TextReference
@ -73,44 +75,37 @@ fun InputRowRecipient(
color = color,
)
}
Row(
Box(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8),
) {
AnimatedContent(
targetState = isLoading,
label = "Indicator Show Change",
modifier = Modifier
.align(CenterVertically)
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
.size(TangemTheme.dimens.size36)
.background(TangemTheme.colors.background.tertiary),
) { showIndicator ->
if (showIndicator) {
CircularProgressIndicator(
color = TangemTheme.colors.icon.informative,
modifier = Modifier
.padding(TangemTheme.dimens.spacing8),
)
} else {
IdentIcon(address = value)
}
Row {
InputIcon(
isLoading = isLoading,
value = value,
)
SimpleTextField(
value = value,
placeholder = placeholder,
onValueChange = onValueChange,
singleLine = singleLine,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12)
.weight(1f)
.align(CenterVertically),
)
CrossIcon(
onClick = onPasteClick,
modifier = Modifier
.align(CenterVertically)
.padding(start = TangemTheme.dimens.spacing8),
)
}
SimpleTextField(
value = value,
placeholder = placeholder,
onValueChange = onValueChange,
singleLine = singleLine,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12)
.weight(1f)
.align(CenterVertically),
)
PasteButton(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
modifier = Modifier
.align(CenterVertically)
.align(CenterEnd)
.padding(start = TangemTheme.dimens.spacing8),
)
}
@ -118,6 +113,29 @@ fun InputRowRecipient(
}
}
@Composable
private fun RowScope.InputIcon(isLoading: Boolean, value: String) {
AnimatedContent(
targetState = isLoading,
label = "Indicator Show Change",
modifier = Modifier
.align(CenterVertically)
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
.size(TangemTheme.dimens.size36)
.background(TangemTheme.colors.background.tertiary),
) { showIndicator ->
if (showIndicator) {
CircularProgressIndicator(
color = TangemTheme.colors.icon.informative,
modifier = Modifier
.padding(TangemTheme.dimens.spacing8),
)
} else {
IdentIcon(address = value)
}
}
}
//region preview
@Preview
@Composable

View file

@ -1,24 +1,28 @@
package com.tangem.core.ui.components.inputrow.inner
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION
/**
* Paste button with cross icon. Retrieves text from clipboard.
@ -33,48 +37,67 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi
val clipboardManager = LocalClipboardManager.current
val hapticFeedback = LocalHapticFeedback.current
if (isPasteButtonVisible) {
Box(modifier = modifier) {
Text(
text = "Paste",
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary2,
modifier = Modifier
.background(
color = TangemTheme.colors.button.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(
horizontal = TangemTheme.dimens.spacing10,
vertical = TangemTheme.dimens.spacing2,
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius8),
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onClick(
clipboardManager
.getText()
?.toString()
.orEmpty(),
)
},
),
)
}
} else {
Icon(
painter = painterResource(id = R.drawable.ic_close_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = stringResource(R.string.common_close),
modifier = modifier
.size(TangemTheme.dimens.size20)
AnimatedVisibility(
visible = isPasteButtonVisible,
label = "Paste Button Visibility Animation",
enter = fadeIn(),
exit = fadeOut(animationSpec = tween(DEFAULT_ANIMATION_DURATION)),
modifier = modifier,
) {
Text(
text = "Paste",
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary2,
modifier = Modifier
.background(
color = TangemTheme.colors.button.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(
horizontal = TangemTheme.dimens.spacing10,
vertical = TangemTheme.dimens.spacing2,
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius10),
onClick = { onClick("") },
indication = rememberRipple(radius = TangemTheme.dimens.radius8),
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onClick(
clipboardManager
.getText()
?.toString()
.orEmpty(),
)
},
),
)
}
}
@Composable
fun CrossIcon(onClick: (String) -> Unit, modifier: Modifier = Modifier) {
Icon(
painter = painterResource(id = R.drawable.ic_close_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = stringResource(R.string.common_close),
modifier = modifier
.size(TangemTheme.dimens.size20)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius10),
onClick = { onClick("") },
),
)
}
@Preview
@Composable
private fun PasteButtonPreview() {
var isVisible by remember { mutableStateOf(true) }
TangemTheme {
PasteButton(
isPasteButtonVisible = isVisible,
onClick = { isVisible = !isVisible },
)
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.core.ui.components.marketprice
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.*
@ -35,39 +34,60 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
*/
@Composable
fun MarketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier) {
var rootWidth by remember { mutableIntStateOf(value = 0) }
Column(
Row(
modifier = modifier
.background(
color = TangemTheme.colors.background.primary,
shape = RoundedCornerShape(TangemTheme.dimens.radius14),
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.heightIn(min = TangemTheme.dimens.size70)
.fillMaxWidth()
.padding(all = TangemTheme.dimens.spacing14)
.onSizeChanged { rootWidth = it.width },
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6),
horizontalAlignment = Alignment.Start,
.heightIn(min = TangemTheme.dimens.size72)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Title(currencyName = state.currencySymbol)
var rootWidth by remember { mutableIntStateOf(value = 0) }
Content(state = state, rootWidth = rootWidth)
Column(
modifier = Modifier.onSizeChanged { rootWidth = it.width },
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
horizontalAlignment = Alignment.Start,
) {
Title(currencyName = state.currencySymbol)
Content(state = state, rootWidth = rootWidth)
}
Icon(
modifier = Modifier.size(TangemTheme.dimens.size20),
painter = painterResource(id = R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
}
@Composable
private fun Title(currencyName: String) {
Text(
text = stringResource(id = R.string.wallet_marketplace_block_title, currencyName),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
)
private fun Title(currencyName: String, modifier: Modifier = Modifier) {
Box(
modifier = modifier.heightIn(min = TangemTheme.dimens.size24),
contentAlignment = Alignment.CenterStart,
) {
Text(
text = stringResource(id = R.string.wallet_marketplace_block_title, currencyName),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
)
}
}
@Composable
private fun Content(state: MarketPriceBlockState, rootWidth: Int) {
AnimatedContent(targetState = state, label = "Update the content") { marketPriceBlockState ->
private fun Content(state: MarketPriceBlockState, rootWidth: Int, modifier: Modifier = Modifier) {
AnimatedContent(
modifier = modifier.heightIn(min = TangemTheme.dimens.size20),
targetState = state,
contentAlignment = Alignment.CenterStart,
label = "Update the content",
) { marketPriceBlockState ->
when (marketPriceBlockState) {
is MarketPriceBlockState.Content,
is MarketPriceBlockState.Error,
@ -97,6 +117,7 @@ private fun PriceContent(state: MarketPriceBlockState, priceWidthDp: Dp) {
@Composable
private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) {
val priceModifier = Modifier.widthIn(max = priceWidthDp)
AnimatedContent(targetState = state, label = "Update the price block") { marketPriceBlockState ->
if (marketPriceBlockState is MarketPriceBlockState.Content) {
Row(
@ -127,12 +148,17 @@ private fun Price(price: String, modifier: Modifier = Modifier) {
@Composable
private fun PriceChangeInPercent(config: PriceChangeState.Content) {
AnimatedContent(targetState = config.type, label = "Update price change") { type ->
AnimatedContent(
targetState = config.type,
contentAlignment = Alignment.CenterStart,
label = "Update price change",
) { type ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4),
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2),
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size8),
painter = painterResource(
id = when (type) {
PriceChangeType.UP -> R.drawable.ic_arrow_up_8
@ -184,7 +210,8 @@ private fun QuoteTimeStatus() {
)
}
@Preview
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun Preview_MarketPriceBlock_Light(
@PreviewParameter(WalletMarketPriceBlockStateProvider::class)
@ -195,7 +222,7 @@ private fun Preview_MarketPriceBlock_Light(
}
}
@Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun Preview_MarketPriceBlock_Dark(
@PreviewParameter(WalletMarketPriceBlockStateProvider::class)
@ -227,4 +254,5 @@ private class WalletMarketPriceBlockStateProvider : CollectionPreviewParameterPr
MarketPriceBlockState.Loading(currencySymbol = "BTC"),
MarketPriceBlockState.Error(currencySymbol = "BTC"),
),
)
)
// endregion Preview

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
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
@ -30,6 +31,7 @@ import com.tangem.core.ui.components.transactions.state.TransactionState.Content
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import java.util.UUID
@ -46,16 +48,21 @@ import java.util.UUID
[REDACTED_AUTHOR]
*/
@Composable
@Suppress("LongMethod")
fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
Surface(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.defaultMinSize(minHeight = TangemTheme.dimens.size56)
.clickable(
enabled = state is TransactionState.Content,
onClick = (state as? TransactionState.Content)?.onClick ?: {},
)
.padding(horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing10),
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size56)
.padding(
vertical = TangemTheme.dimens.spacing8,
horizontal = TangemTheme.dimens.spacing12,
),
color = TangemTheme.colors.background.primary,
) {
@Suppress("DestructuringDeclarationWithTooManyEntries")
@ -76,21 +83,30 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod
Title(
state = state,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing12)
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing4,
)
.constrainAs(titleItem) {
top.linkTo(parent.top)
bottom.linkTo(subtitleItem.top)
start.linkTo(iconItem.end)
top.linkTo(iconItem.top)
end.linkTo(amountItem.start)
width = Dimension.fillToConstraints
},
)
Subtitle(
state = state,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing6)
.padding(horizontal = TangemTheme.dimens.spacing12)
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing4,
)
.constrainAs(subtitleItem) {
top.linkTo(titleItem.bottom)
bottom.linkTo(parent.bottom)
start.linkTo(iconItem.end)
top.linkTo(amountItem.bottom)
end.linkTo(timestampItem.start)
width = Dimension.fillToConstraints
},
@ -100,8 +116,9 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod
state = state,
isBalanceHidden = isBalanceHidden,
modifier = Modifier.constrainAs(amountItem) {
top.linkTo(parent.top)
bottom.linkTo(timestampItem.top)
start.linkTo(titleItem.end)
top.linkTo(titleItem.top)
end.linkTo(parent.end)
width = Dimension.fillToConstraints
},
@ -109,12 +126,11 @@ fun Transaction(state: TransactionState, isBalanceHidden: Boolean, modifier: Mod
Timestamp(
state = state,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing6)
.constrainAs(timestampItem) {
top.linkTo(amountItem.bottom)
end.linkTo(parent.end)
},
modifier = Modifier.constrainAs(timestampItem) {
top.linkTo(amountItem.bottom)
bottom.linkTo(parent.bottom)
end.linkTo(parent.end)
},
)
}
}
@ -173,11 +189,16 @@ private fun Icon(state: TransactionState, modifier: Modifier = Modifier) {
private fun Title(state: TransactionState, modifier: Modifier = Modifier) {
when (state) {
is TransactionState.Content -> {
Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6)) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6),
) {
Text(
text = state.title.resolveReference(),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
if (state.status is Status.Unconfirmed) {
@ -266,7 +287,7 @@ private fun Timestamp(state: TransactionState, modifier: Modifier = Modifier) {
when (state) {
is TransactionState.Content -> {
Text(
text = state.timestamp,
text = state.time,
modifier = modifier,
textAlign = TextAlign.End,
color = TangemTheme.colors.text.tertiary,
@ -321,89 +342,109 @@ private class TransactionItemStateProvider : CollectionPreviewParameterProvider<
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "-0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Confirmed,
direction = Direction.OUTGOING,
iconRes = R.drawable.ic_arrow_up_24,
title = resourceReference(R.string.common_transfer),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "+0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Unconfirmed,
direction = Direction.INCOMING,
iconRes = R.drawable.ic_arrow_down_24,
title = resourceReference(R.string.common_transfer),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "+0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Unconfirmed,
direction = Direction.OUTGOING,
iconRes = R.drawable.ic_doc_24,
title = resourceReference(R.string.common_approval),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "+0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Failed,
direction = Direction.OUTGOING,
iconRes = R.drawable.ic_doc_24,
title = resourceReference(R.string.common_approval),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "+0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Confirmed,
direction = Direction.OUTGOING,
iconRes = R.drawable.ic_doc_24,
title = resourceReference(R.string.common_approval),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "+0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Unconfirmed,
direction = Direction.INCOMING,
iconRes = R.drawable.ic_arrow_down_24,
title = resourceReference(R.string.common_swap),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "+0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Confirmed,
direction = Direction.INCOMING,
iconRes = R.drawable.ic_doc_24,
title = TextReference.Str("Submit"),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "+0.500913 BTC",
timestamp = "8:41",
time = "8:41",
status = Status.Confirmed,
direction = Direction.OUTGOING,
iconRes = R.drawable.ic_arrow_up_24,
title = TextReference.Str("Submit"),
subtitle = TextReference.Str("33BddS...ga2B"),
timestamp = 0L,
onClick = {},
),
TransactionState.Content(
txHash = UUID.randomUUID().toString(),
amount = "0.625 USDT",
time = "€0.50",
status = Status.Confirmed,
direction = Direction.OUTGOING,
iconRes = R.drawable.ic_arrow_up_24,
title = TextReference.Str("Unlimint Banking Cards Sandbox London"),
subtitle = stringReference(value = "8:41 • authorized"),
timestamp = 0L,
onClick = {},
),
TransactionState.Loading(txHash = UUID.randomUUID().toString()),

View file

@ -1,12 +1,14 @@
package com.tangem.core.ui.components.transactions
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState
import com.tangem.core.ui.res.TangemTheme
@ -20,19 +22,23 @@ import java.util.UUID
*/
@Composable
internal fun TxHistoryGroupTitle(config: TxHistoryItemState.GroupTitle, modifier: Modifier = Modifier) {
Text(
text = config.title,
Box(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.fillMaxWidth()
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing14,
),
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Start,
style = TangemTheme.typography.body2,
)
vertical = TangemTheme.dimens.spacing8,
horizontal = TangemTheme.dimens.spacing12,
)
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size24),
contentAlignment = Alignment.CenterStart,
) {
Text(
text = config.title,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
)
}
}
@Preview

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -24,9 +25,11 @@ internal fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Mod
Row(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing16),
.padding(horizontal = TangemTheme.dimens.spacing12)
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size24),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
@ -37,13 +40,14 @@ internal fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Mod
Row(
modifier = Modifier.clickable(onClick = onExploreClick),
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2),
) {
Icon(
modifier = Modifier.size(size = TangemTheme.dimens.size20),
painter = painterResource(id = R.drawable.ic_compass_24),
contentDescription = null,
modifier = Modifier.size(size = TangemTheme.dimens.size18),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
Text(
text = stringResource(id = R.string.common_explorer),

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