Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-12 15:50:29 +03:00
commit cef36091da
379 changed files with 8948 additions and 8976 deletions

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

@ -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
@ -57,8 +62,20 @@ object TradeCryptoMiddleware {
is TradeCryptoAction.Swap -> openSwap(
currency = action.cryptoCurrency,
)
is TradeCryptoAction.SendToken -> handleSendToken(action = action)
is TradeCryptoAction.SendCoin -> handleSendCoin(action = action)
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 +297,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))
}
}