Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-01 15:07:41 +03:00
commit 94f1dfa29e
63 changed files with 878 additions and 555 deletions

View file

@ -87,7 +87,7 @@ internal class DeepLinkFactory @Inject constructor(
fun checkRoutingReadiness(appRoute: AppRoute) {
permittedAppRoute.value = when (appRoute) {
AppRoute.Initial,
AppRoute.Home,
is AppRoute.Home,
is AppRoute.Welcome,
is AppRoute.PushNotification,
is AppRoute.Disclaimer,

View file

@ -13,6 +13,7 @@ import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
@ -97,7 +98,9 @@ class AmountStateConverter(
* @property maxEnterAmount max enter amount data
* @property cryptoCurrencyStatus current cryptocurrency status
* @property iconStateConverter currency icon converter
* @property isBalanceHidden is balance hidden status
*/
@Suppress("LongParameterList")
class AmountStateConverterV2(
private val clickIntents: AmountScreenClickIntents,
private val appCurrency: AppCurrency,
@ -105,6 +108,7 @@ class AmountStateConverterV2(
private val maxEnterAmount: EnterAmountBoundary,
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
private val isRedesignEnabled: Boolean,
private val isBalanceHidden: Boolean,
) : Converter<AmountParameters, AmountState> {
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
@ -120,6 +124,12 @@ class AmountStateConverterV2(
val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
val noFeeRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero()
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
return AmountState.Empty(
isRedesignEnabled = isRedesignEnabled,
)
}
return AmountState.Data(
title = value.title,
availableBalance = if (isRedesignEnabled) {
@ -127,11 +137,12 @@ class AmountStateConverterV2(
stringReference(crypto),
stringReference(" $DOT "),
stringReference(fiat),
)
).orMaskWithStars(isBalanceHidden)
} else {
resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat))
.orMaskWithStars(isBalanceHidden)
},
availableBalanceShort = stringReference(crypto),
availableBalanceShort = stringReference(crypto).orMaskWithStars(isBalanceHidden),
tokenName = stringReference(cryptoCurrencyStatus.currency.name),
tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus.currency),
amountTextField = amountFieldConverter.convert(value.value),

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
@ -28,6 +29,7 @@ class AmountBoundaryUpdateTransformer(
private val maxEnterAmount: EnterAmountBoundary,
private val appCurrency: AppCurrency,
private val isRedesignEnabled: Boolean,
private val isBalanceHidden: Boolean,
) : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState {
@ -47,8 +49,8 @@ class AmountBoundaryUpdateTransformer(
}
return prevState.copy(
availableBalance = availableBalance,
availableBalanceShort = stringReference(crypto),
availableBalance = availableBalance.orMaskWithStars(isBalanceHidden),
availableBalanceShort = stringReference(crypto).orMaskWithStars(isBalanceHidden),
)
}
}

View file

@ -39,7 +39,7 @@ class AmountFieldChangeTransformer(
val amountTextField = prevState.amountTextField
if (value.isEmpty()) return prevState.emptyState()
if (value.isEmpty()) return prevState.emptyState(maxEnterAmount.fiatRate)
val cryptoDecimals = amountTextField.cryptoAmount.decimals
val fiatDecimals = amountTextField.fiatAmount.decimals
@ -75,7 +75,7 @@ class AmountFieldChangeTransformer(
error = when {
isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance)
isLessThanMinimumIfProvided -> {
val minimumAmount = minimumTransactionAmount?.amount.format {
val minimumAmount = minimumTransactionAmount.amount.format {
crypto(cryptoCurrencyStatus.currency)
}
@ -96,7 +96,7 @@ class AmountFieldChangeTransformer(
)
}
private fun AmountState.Data.emptyState(): AmountState.Data {
private fun AmountState.Data.emptyState(fiatRate: BigDecimal?): AmountState.Data {
return copy(
isPrimaryButtonEnabled = false,
reduceAmountBy = BigDecimal.ZERO,
@ -104,7 +104,7 @@ class AmountFieldChangeTransformer(
value = "",
fiatValue = "",
cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO),
fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO),
fiatAmount = amountTextField.fiatAmount.copy(value = if (fiatRate != null) BigDecimal.ZERO else null),
isError = false,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.None,

View file

@ -100,8 +100,8 @@ class AmountFieldConverterV2(
val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency)
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val (fiatValue, fiatDecimal) = when {
value.isEmpty() -> "" to BigDecimal.ZERO
fiatRate.isNullOrZero() -> "" to null
value.isEmpty() -> "" to BigDecimal.ZERO
else -> {
val fiatDecimal = fiatRate?.multiply(cryptoDecimal)
val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty()

View file

@ -93,6 +93,13 @@ object AmountStatePreviewData {
amountTextField = amountWithValueState.amountTextField.copy(isFiatValue = false),
)
val amountStateV2WithoutRates = amountState.copy(
amountTextField = amountState.amountTextField.copy(
fiatAmount = amountState.amountTextField.fiatAmount.copy(
value = null,
),
),
)
val amountErrorState = amountWithValueState.copy(
amountTextField = amountWithValueState.amountTextField.copy(
isError = true,

View file

@ -19,6 +19,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.BottomCenter
import androidx.compose.ui.Alignment.Companion.TopCenter
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.graphicsLayer
@ -141,7 +142,6 @@ private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -
AmountFieldCurrencyInfo(
amountUM = amountUM,
onCurrencyChange = onCurrencyChange,
)
AmountFieldError(
isError = amountUM.amountTextField.isError,
@ -157,6 +157,8 @@ private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -
@Composable
private fun BoxScope.AmountFieldCurrencyInfo(amountUM: AmountState.Data, onCurrencyChange: (Boolean) -> Unit) {
val isFiatAvailable = amountUM.amountTextField.fiatAmount.value != null
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
@ -168,6 +170,7 @@ private fun BoxScope.AmountFieldCurrencyInfo(amountUM: AmountState.Data, onCurre
indication = null,
onClick = { onCurrencyChange(!amountUM.amountTextField.isFiatValue) },
)
.alpha(if (isFiatAvailable) 1f else 0f)
.padding(4.dp),
) {
val iconRotateState by animateFloatAsState(
@ -327,6 +330,7 @@ private class AmountFieldV2PreviewProvider : PreviewParameterProvider<AmountStat
AmountStatePreviewData.emptyState,
AmountStatePreviewData.amountState,
AmountStatePreviewData.amountErrorState,
AmountStatePreviewData.amountStateV2WithoutRates,
)
}
// endregion

View file

@ -304,6 +304,8 @@
<string name="details_title">Подробности</string>
<string name="disclaimer_error_loading">Проверьте подключение с интернетом или переключитесь на другую сеть</string>
<string name="disclaimer_title">Условия использования</string>
<string name="domain_receive_assets_legacy_address">Legacy адрес</string>
<string name="domain_receive_assets_onboarding_description">Отправка средств в другой сети может повлечь потерю средств.</string>
<string name="email_preface_wc_error">Привет, команда поддержки, у меня возникла ошибка с кодом: %s</string>
<string name="email_subject_wc_error">Ошибка WalletConnect</string>
<string name="error_wrong_wallet_tapped">Вы использовали карту или кольцо от другого кошелька. Приложите карту или кольцо, связанную с этим кошельком.</string>
@ -858,7 +860,7 @@
<string name="send_gas_limit_footer">Это максимальное количество газа, которое будет потрачено на выполнение транзакции или контракта. Лимит газа предотвращает неожиданные или неограниченные расходы при выполнении транзакции.</string>
<string name="send_gas_price">Цена газа</string>
<string name="send_gas_price_footer">Это стоимость, которую вы готовы заплатить за каждую единицу газа. Чем выше цена газа, тем быстрее ваша транзакция будет обработана.</string>
<string name="send_max_amount">Всё</string>
<string name="send_max_amount">Макс</string>
<string name="send_max_amount_label">Максимальная сумма</string>
<string name="send_max_fee">Комиссия не превысит</string>
<string name="send_memo_destination_tag_error">Недопустимый Memo</string>
@ -1081,6 +1083,7 @@
<string name="swap_story_second_title">Лучшие курсы</string>
<string name="swap_story_third_subtitle">Интуитивный обмен в пару касаний — без сложностей и ожидания</string>
<string name="swap_story_third_title">Проще простого</string>
<string name="swap_via_provider">Обмен через провайдера</string>
<string name="swapping_alert_cex_description">В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя.</string>
<string name="swapping_alert_cex_description_with_slippage">В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %1$s от биржи обратно на адрес пользователя \n\nПроскальзывание провайдера составляет до %2$s</string>
<string name="swapping_alert_dex_description">В сумму включена комиссия провайдера сервиса.</string>

View file

@ -393,6 +393,7 @@
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
<string name="disclaimer_title">Terms of service</string>
<string name="domain_receive_assets_default_address">Default Address</string>
<string name="domain_receive_assets_legacy_address">Legacy Address</string>
<string name="domain_receive_assets_navigation_title">Receive assets</string>
<string name="domain_receive_assets_onboarding_description">Sending assets in other networks will result in permanent loss.</string>
<string name="domain_receive_assets_onboarding_network_name">%s network</string>
@ -985,6 +986,7 @@
<string name="send_extras_hint_memo">Memo</string>
<string name="send_fee_unreachable_error_text">Check your network connection</string>
<string name="send_fee_unreachable_error_title">Network fee info unreachable</string>
<string name="send_from_title">You send</string>
<string name="send_from_wallet_android">From</string>
<string name="send_from_wallet_name">From %s</string>
<string name="send_gas_limit">Gas limit</string>

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
@ -30,6 +31,7 @@ import com.tangem.core.ui.res.TangemThemePreview
* @param shape icon button shape
* @param background background color
* @param iconTint icon color
* @param innerPadding icon padding inside background area
*
* [Show in Figma](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=4105-1439&t=nnYBX1qCZmUNhBDf-4)
*/
@ -41,6 +43,7 @@ fun TangemIconButton(
shape: RoundedCornerShape = RoundedCornerShape(24.dp),
background: Color = TangemTheme.colors.button.secondary,
iconTint: Color = TangemTheme.colors.icon.secondary,
innerPadding: Dp = 4.dp,
) {
Icon(
painter = rememberVectorPainter(ImageVector.vectorResource(iconRes)),
@ -50,7 +53,7 @@ fun TangemIconButton(
.size(24.dp)
.clip(shape)
.background(background)
.padding(4.dp)
.padding(innerPadding)
.clickable(
onClick = onClick,
),

View file

@ -240,15 +240,19 @@ internal class DefaultTransactionRepository(
val validator = walletManager as? TransactionValidator
if (validator != null) {
val transactionData = walletManager.createTransaction(
amount = amount,
fee = fee ?: Fee.Common(amount = amount),
destination = destination,
).copy(
extras = getMemoExtras(networkId = network.rawId, memo = memo),
)
try {
val transactionData = walletManager.createTransaction(
amount = amount,
fee = fee ?: Fee.Common(amount = amount),
destination = destination,
).copy(
extras = getMemoExtras(networkId = network.rawId, memo = memo),
)
validator.validate(transactionData = transactionData)
validator.validate(transactionData = transactionData)
} catch (ex: Exception) {
Result.failure(ex)
}
} else {
Timber.e("${walletManager?.wallet?.blockchain} does not support transaction validation")
Result.success(Unit)

View file

@ -31,7 +31,7 @@ class DefaultWalletAddressServiceRepository(
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
walletManager?.wallet?.ens
walletManager?.wallet?.ens.takeIf { it.isNullOrEmpty().not() }
}
}
@ -50,7 +50,7 @@ class DefaultWalletAddressServiceRepository(
)
if (walletManager is NameResolver) {
walletManager.reverseResolve(address.toByteArray())
walletManager.reverseResolve(address)
} else {
ReverseResolveAddressResult.NotSupported
}

View file

@ -19,10 +19,9 @@ data class TokenReceiveConfig(
data class ReceiveAddressModel(
val nameService: NameService,
val value: String,
val displayName: String,
) {
enum class NameService {
Default, Ens
Default, Legacy, Ens
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
sealed class TokenReceiveNewAnalyticsEvent(
@ -27,11 +28,13 @@ sealed class TokenReceiveNewAnalyticsEvent(
class ButtonCopyAddress(
token: String,
blockchainName: String,
tokenReceiveSource: TokenReceiveCopyActionSource,
) : TokenReceiveNewAnalyticsEvent(
event = "Button - Copy Address",
params = mapOf(
TOKEN_PARAM to token,
BLOCKCHAIN to blockchainName,
SOURCE to tokenReceiveSource.name,
),
)
@ -56,4 +59,8 @@ sealed class TokenReceiveNewAnalyticsEvent(
BLOCKCHAIN to blockchainName,
),
)
}
enum class TokenReceiveCopyActionSource {
Main, Token, Receive, QR
}

View file

@ -35,7 +35,13 @@ class GetReverseResolvedEnsAddressUseCase(private val walletAddressServiceReposi
return when (reverseResolveAddressResult) {
is ReverseResolveAddressResult.Error -> EnsAddress.Error(reverseResolveAddressResult.error)
ReverseResolveAddressResult.NotSupported -> EnsAddress.NotSupported
is ReverseResolveAddressResult.Resolved -> EnsAddress.Address(reverseResolveAddressResult.name)
is ReverseResolveAddressResult.Resolved -> {
if (reverseResolveAddressResult.name.isEmpty()) {
EnsAddress.NotSupported
} else {
EnsAddress.Address(reverseResolveAddressResult.name)
}
}
}
}
}

View file

@ -21,14 +21,12 @@ class ValidateTransactionUseCase(
destination: String,
userWalletId: UserWalletId,
network: Network,
): Either<Throwable, Unit> = Either.catch {
transactionRepository.validateTransaction(
amount = amount,
fee = fee,
memo = memo,
destination = destination,
userWalletId = userWalletId,
network = network,
).fold(onSuccess = { Unit.right() }, onFailure = { it.left() })
}
): Either<Throwable, Unit> = transactionRepository.validateTransaction(
amount = amount,
fee = fee,
memo = memo,
destination = destination,
userWalletId = userWalletId,
network = network,
).fold(onSuccess = { Unit.right() }, onFailure = { it.left() })
}

View file

@ -28,6 +28,7 @@ import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.core.wallets.error.SaveWalletError
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
@ -75,6 +76,7 @@ internal class HomeModel @Inject constructor(
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
private val urlOpener: UrlOpener,
private val userWalletsListManager: UserWalletsListManager,
private val reduxStateHolder: ReduxStateHolder,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
) : Model() {
@ -206,6 +208,7 @@ internal class HomeModel @Inject constructor(
}
},
ifRight = {
reduxStateHolder.onUserWalletSelected(userWallet)
setLoading(false)
sendSignedInCardAnalyticsEvent(scanResponse)
appRouter.replaceAll(AppRoute.Wallet)

View file

@ -26,6 +26,7 @@ import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
@ -390,16 +391,17 @@ internal class MarketsPortfolioModel @Inject constructor(
ReceiveAddressModel(
nameService = ReceiveAddressModel.NameService.Ens,
value = ens,
displayName = ens,
),
)
}
addresses.availableAddresses.map { address ->
add(
ReceiveAddressModel(
nameService = ReceiveAddressModel.NameService.Default,
nameService = when (address.type) {
NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.NameService.Default
NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.NameService.Legacy
},
value = address.value,
displayName = "${cryptoCurrency.name} (${cryptoCurrency.symbol})",
),
)
}

View file

@ -209,16 +209,17 @@ internal class NFTReceiveModel @Inject constructor(
ReceiveAddressModel(
nameService = ReceiveAddressModel.NameService.Ens,
value = ens,
displayName = ens,
),
)
}
addresses.availableAddresses.map { address ->
add(
ReceiveAddressModel(
nameService = ReceiveAddressModel.NameService.Default,
nameService = when (address.type) {
NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.NameService.Default
NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.NameService.Legacy
},
value = address.value,
displayName = cryptoCurrency.symbol,
),
)
}

View file

@ -1,6 +1,8 @@
package com.tangem.features.send.v2.api.subcomponents.amount.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE
sealed class CommonSendAmountAnalyticEvents(
@ -22,7 +24,16 @@ sealed class CommonSendAmountAnalyticEvents(
/** Max amount button clicked */
data class MaxAmountButtonClicked(
val categoryName: String,
) : CommonSendAmountAnalyticEvents(category = categoryName, event = "Max Amount Taped")
val token: String,
val blockchain: String,
) : CommonSendAmountAnalyticEvents(
category = categoryName,
event = "Max Amount Taped",
params = mapOf(
TOKEN_PARAM to token,
BLOCKCHAIN to blockchain,
),
)
enum class SelectedCurrencyType(val value: String) {
Token("Token"),

View file

@ -69,6 +69,9 @@ internal class SendEntryPointModel @Inject constructor(
}
override fun onBack() {
router.pop()
modelScope.launch {
sendAmountUpdateTrigger.triggerUpdateAmount(lastSavedAmount)
router.pop()
}
}
}

View file

@ -24,7 +24,6 @@ internal sealed class SendAnalyticEvents(
val feeType: AnalyticsParam.FeeType,
val blockchain: String,
val nonceNotEmpty: Boolean,
private val ensStatus: AnalyticsParam.EnsStatus,
) : SendAnalyticEvents(
event = "Transaction Sent Screen Opened",
params = mapOf(
@ -32,10 +31,7 @@ internal sealed class SendAnalyticEvents(
FEE_TYPE to feeType.value,
BLOCKCHAIN to blockchain,
NONCE to nonceNotEmpty.toString().capitalize(),
ENS_ADDRESS to when (ensStatus) {
AnalyticsParam.EnsStatus.EMPTY -> false.toString()
AnalyticsParam.EnsStatus.FULL -> true.toString()
},
ENS_ADDRESS to (blockchain == "Ethereum").toString(),
),
)

View file

@ -28,7 +28,6 @@ internal class SendAnalyticHelper @Inject constructor(
feeType = feeType,
blockchain = cryptoCurrency.network.name,
nonceNotEmpty = feeSelectorUM.nonce != null,
ensStatus = getEnsStatus(sendUM),
),
)
analyticsEventHandler.send(
@ -53,14 +52,4 @@ internal class SendAnalyticHelper @Inject constructor(
else -> Basic.TransactionSent.MemoType.Null
}
}
private fun getEnsStatus(sendUM: SendUM): AnalyticsParam.EnsStatus {
val blockchainAddressForEns =
(sendUM.destinationUM as? DestinationUM.Content)?.addressTextField?.blockchainAddress
return if (blockchainAddressForEns != null) {
AnalyticsParam.EnsStatus.FULL
} else {
AnalyticsParam.EnsStatus.EMPTY
}
}
}

View file

@ -72,6 +72,7 @@ internal class SendConfirmComponent(
userWalletId = params.userWallet.walletId,
cryptoCurrency = params.cryptoCurrencyStatus.currency,
cryptoCurrencyStatusFlow = params.cryptoCurrencyStatusFlow,
isBalanceHidingFlow = params.isBalanceHidingFlow,
),
onResult = model::onAmountResult,
onClick = model::showEditAmount,

View file

@ -28,6 +28,7 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
import com.tangem.domain.settings.NeverShowTapHelpUseCase
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
@ -105,6 +106,7 @@ internal class SendConfirmModel @Inject constructor(
private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger,
private val feeReloadTrigger: SendFeeReloadTrigger,
private val sendAmountReduceTrigger: SendAmountReduceTrigger,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
sendBalanceUpdaterFactory: SendBalanceUpdater.Factory,
) : Model(), SendConfirmClickIntents, FeeSelectorModelCallback, SendNotificationsComponent.ModelCallback {
@ -121,6 +123,9 @@ internal class SendConfirmModel @Inject constructor(
private val _uiState = MutableStateFlow(params.state)
val uiState = _uiState.asStateFlow()
val isBalanceHiddenFlow: StateFlow<Boolean>
field = MutableStateFlow(false)
private val amountState
get() = uiState.value.amountUM as? AmountState.Data
private val destinationUM
@ -139,8 +144,16 @@ internal class SendConfirmModel @Inject constructor(
reduceAmountBy = amountState?.reduceAmountBy.orZero(),
isIgnoreReduce = amountState?.isIgnoreReduce == true,
enteredDestination = destinationUM?.addressTextField?.actualAddress,
fee = feeSelectorUM?.selectedFee,
feeError = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error,
fee = if (uiState.value.isRedesignEnabled) {
feeUMV2?.selectedFeeItem?.fee
} else {
feeSelectorUM?.selectedFee
},
feeError = if (uiState.value.isRedesignEnabled) {
(uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Error)?.error
} else {
(feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error
},
)
private var sendIdleTimer: Long = 0L
@ -156,6 +169,7 @@ internal class SendConfirmModel @Inject constructor(
subscribeOnNotificationsUpdateTrigger()
subscribeOnCheckFeeResultUpdates()
initialState()
subscribeOnBalanceHidden()
}
fun updateState(state: SendUM) {
@ -511,6 +525,16 @@ internal class SendConfirmModel @Inject constructor(
}.launchIn(modelScope)
}
private fun subscribeOnBalanceHidden() {
getBalanceHidingSettingsUseCase()
.conflate()
.distinctUntilChanged()
.onEach { balanceHidingSettings ->
isBalanceHiddenFlow.update { balanceHidingSettings.isBalanceHidden }
}
.launchIn(modelScope)
}
private fun updateConfirmNotifications() {
modelScope.launch {
notificationsUpdateTrigger.triggerUpdate(

View file

@ -111,7 +111,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
val stackState by childStack.subscribeAsState()
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = ::onChildBack)
BackHandler(onBack = model::onBackClick)
SendContent(
navigationUM = state.navigationUM,
stackState = stackState,

View file

@ -120,8 +120,16 @@ internal class NFTSendConfirmModel @Inject constructor(
get() = ConfirmData(
enteredDestination = destinationUM?.addressTextField?.actualAddress,
enteredMemo = destinationUM?.memoTextField?.value,
fee = feeSelectorUM?.selectedFee,
feeError = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error,
fee = if (uiState.value.isRedesignEnabled) {
(uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content)?.selectedFeeItem?.fee
} else {
feeSelectorUM?.selectedFee
},
feeError = if (uiState.value.isRedesignEnabled) {
(uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Error)?.error
} else {
(feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error
},
)
private var sendIdleTimer: Long = 0L
@ -255,11 +263,15 @@ internal class NFTSendConfirmModel @Inject constructor(
private fun initialState() {
val confirmUM = uiState.value.confirmUM
val feeUM = uiState.value.feeUM
val isEmptyFee = if (uiState.value.isRedesignEnabled) {
uiState.value.feeSelectorUM !is FeeSelectorUMRedesigned.Content
} else {
uiState.value.feeUM is FeeUM.Empty
}
modelScope.launch {
val isShowTapHelp = isSendTapHelpEnabledUseCase().getOrElse { false }
if (confirmUM is ConfirmUM.Empty || feeUM is FeeUM.Empty) {
if (confirmUM is ConfirmUM.Empty || isEmptyFee) {
_uiState.update {
it.copy(
confirmUM = NFTSendConfirmInitialStateTransformer(
@ -277,11 +289,17 @@ internal class NFTSendConfirmModel @Inject constructor(
notificationsUpdateListener.hasErrorFlow
.onEach { hasError ->
_uiState.update {
val feeUM = it.feeUM as? FeeUM.Content
val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
val isFeeNotNull = if (uiState.value.isRedesignEnabled) {
it.feeSelectorUM is FeeSelectorUMRedesigned.Content
} else {
val feeUM = it.feeUM as? FeeUM.Content
val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
feeSelectorUM != null
}
it.copy(
confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy(
isPrimaryButtonEnabled = !hasError && feeSelectorUM != null,
isPrimaryButtonEnabled = !hasError && isFeeNotNull,
) ?: it.confirmUM,
)
}
@ -290,9 +308,8 @@ internal class NFTSendConfirmModel @Inject constructor(
}
private fun verifyAndSendTransaction() {
val destination = destinationUM?.addressTextField?.actualAddress ?: return
val memo = destinationUM?.memoTextField?.value
val fee = feeSelectorUM?.selectedFee ?: return
val destination = confirmData.enteredDestination ?: return
val fee = confirmData.fee ?: return
val ownerAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: return
val sdkNFTAsset = NFTSdkAssetConverter.convertBack(params.nftAsset)
@ -302,7 +319,7 @@ internal class NFTSendConfirmModel @Inject constructor(
ownerAddress = ownerAddress,
nftAsset = sdkNFTAsset.second,
fee = fee,
memo = memo,
memo = confirmData.enteredMemo,
destinationAddress = destination,
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,

View file

@ -28,6 +28,7 @@ import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.nft.entity.NFTSendSuccessTrigger
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
@ -74,6 +75,7 @@ internal class NFTSendModel @Inject constructor(
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val alertFactory: SendConfirmAlertFactory,
private val sendFeatureToggles: SendFeatureToggles,
private val nftSendSuccessTrigger: NFTSendSuccessTrigger,
) : Model(), SendNFTComponentCallback, NFTSendSuccessComponent.ModelCallback {
val params: NFTSendComponent.Params = paramsContainer.require()
@ -119,6 +121,11 @@ internal class NFTSendModel @Inject constructor(
}
override fun onBackClick() {
if (currentRouteFlow.value == ConfirmSuccess) {
modelScope.launch {
nftSendSuccessTrigger.triggerSuccessNFTSend()
}
}
router.pop()
}

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.navigationButtons.NavigationModelCallback
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
@ -85,7 +86,7 @@ internal class NFTSendSuccessComponent @AssistedInject constructor(
val callback: ModelCallback,
)
interface ModelCallback {
interface ModelCallback : NavigationModelCallback {
fun onResult(nftSendUM: NFTSendUM)
}

View file

@ -1,7 +1,6 @@
package com.tangem.features.send.v2.sendnft.success.model
import androidx.compose.runtime.Stable
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.navigationButtons.NavigationButton
import com.tangem.common.ui.navigationButtons.NavigationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -31,7 +30,6 @@ internal class NFTSendSuccessModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appRouter: AppRouter,
private val urlOpener: UrlOpener,
private val shareManager: ShareManager,
) : Model() {
@ -64,16 +62,14 @@ internal class NFTSendSuccessModel @Inject constructor(
isValid = true,
),
)
appRouter.pop()
params.callback.onBackClick()
},
primaryButton = NavigationButton(
textReference = resourceReference(R.string.common_close),
iconRes = null,
isEnabled = true,
isHapticClick = false,
onClick = {
appRouter.pop()
},
onClick = params.callback::onBackClick,
),
prevButton = null,
secondaryPairButtonsUM = NavigationButton(

View file

@ -21,6 +21,7 @@ internal sealed class SendAmountComponentParams {
abstract val isRedesignEnabled: Boolean
abstract val cryptoCurrency: CryptoCurrency
abstract val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
abstract val isBalanceHidingFlow: StateFlow<Boolean>
data class AmountParams(
override val state: AmountState,
@ -31,9 +32,9 @@ internal sealed class SendAmountComponentParams {
override val isRedesignEnabled: Boolean,
override val cryptoCurrency: CryptoCurrency,
override val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
override val isBalanceHidingFlow: StateFlow<Boolean>,
val callback: ModelCallback,
val currentRoute: StateFlow<CommonSendRoute>,
val isBalanceHidingFlow: StateFlow<Boolean>,
) : SendAmountComponentParams()
data class AmountBlockParams(
@ -45,6 +46,7 @@ internal sealed class SendAmountComponentParams {
override val isRedesignEnabled: Boolean,
override val cryptoCurrency: CryptoCurrency,
override val cryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
override val isBalanceHidingFlow: StateFlow<Boolean>,
val userWallet: UserWallet,
val blockClickEnableFlow: StateFlow<Boolean>,
) : SendAmountComponentParams()

View file

@ -28,6 +28,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.api.entity.PredefinedValues
import com.tangem.features.send.v2.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents
@ -66,6 +67,7 @@ internal class SendAmountModel @Inject constructor(
private val getUserWalletUseCase: GetUserWalletUseCase,
private val rampStateManager: RampStateManager,
private val sendAmountAlertFactory: SendAmountAlertFactory,
private val getWalletsUseCase: GetWalletsUseCase,
) : Model(), SendAmountClickIntents {
private val params: SendAmountComponentParams = paramsContainer.require()
@ -102,6 +104,7 @@ internal class SendAmountModel @Inject constructor(
subscribeOnAmountReduceToTriggerUpdates()
subscribeOnAmountIgnoreReduceTriggerUpdates()
subscribeOnAmountUpdateTriggerUpdates()
subscribeOnBalanceHiddenUpdates()
}
private fun initAppCurrency() {
@ -119,6 +122,20 @@ internal class SendAmountModel @Inject constructor(
}.launchIn(modelScope)
}
private fun subscribeOnBalanceHiddenUpdates() {
params.isBalanceHidingFlow.onEach { isBalanceHidden ->
_uiState.update(
AmountBoundaryUpdateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxEnterAmount = maxAmountBoundary,
appCurrency = appCurrency,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
isBalanceHidden = params.isBalanceHidingFlow.value,
),
)
}.launchIn(modelScope)
}
private fun subscribeOnCryptoCurrencyStatusFlow() {
params.cryptoCurrencyStatusFlow
.onEach { newCryptoCurrencyStatus ->
@ -150,6 +167,7 @@ internal class SendAmountModel @Inject constructor(
maxEnterAmount = maxAmountBoundary,
appCurrency = appCurrency,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
isBalanceHidden = params.isBalanceHidingFlow.value,
),
)
} else {
@ -160,6 +178,7 @@ internal class SendAmountModel @Inject constructor(
private fun initialState() {
if (uiState.value is AmountState.Empty && userWallet != null) {
val isSingleWallet = getWalletsUseCase.invokeSync().size == 1
_uiState.update {
AmountStateConverterV2(
clickIntents = this,
@ -168,12 +187,17 @@ internal class SendAmountModel @Inject constructor(
maxEnterAmount = maxAmountBoundary,
iconStateConverter = CryptoCurrencyToIconStateConverter(),
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
isBalanceHidden = params.isBalanceHidingFlow.value,
).convert(
AmountParameters(
title = resourceReference(
R.string.send_from_wallet_name,
WrappedList(listOf(userWallet?.name.orEmpty())), // TODO [REDACTED_TASK_KEY]
),
title = if (isSingleWallet) {
resourceReference(R.string.send_from_title)
} else {
resourceReference(
R.string.send_from_wallet_name,
WrappedList(listOf(userWallet?.name.orEmpty())), // TODO [REDACTED_TASK_KEY]
)
},
value = "",
),
)
@ -218,7 +242,11 @@ internal class SendAmountModel @Inject constructor(
),
)
analyticsEventHandler.send(
CommonSendAmountAnalyticEvents.MaxAmountButtonClicked(categoryName = analyticsCategoryName),
CommonSendAmountAnalyticEvents.MaxAmountButtonClicked(
categoryName = analyticsCategoryName,
token = params.cryptoCurrency.symbol,
blockchain = params.cryptoCurrency.network.name,
),
)
}
@ -257,6 +285,11 @@ internal class SendAmountModel @Inject constructor(
private fun confirmConvertToToken() {
val amountParams = params as? SendAmountComponentParams.AmountParams ?: return
val amountFieldData = uiState.value as? AmountState.Data
_uiState.update {
(it as? AmountState.Data)?.copy(
isPrimaryButtonEnabled = false,
) ?: it
}
amountParams.callback.onConvertToAnotherToken(amountFieldData?.amountTextField?.value.orEmpty())
}

View file

@ -106,8 +106,8 @@ internal class SendDestinationModel @Inject constructor(
),
)
}
initSenderAddress()
}
initSenderAddress()
}
fun updateState(destinationUM: DestinationUM) {

View file

@ -287,7 +287,7 @@ internal class NotificationsModel @Inject constructor(
) {
val validationError = validateTransactionUseCase(
userWalletId = userWalletId,
amount = sendingAmount.convertToSdkAmount(currency),
amount = enteredAmount.convertToSdkAmount(currency),
fee = fee,
memo = memo,
destination = destinationAddress,

View file

@ -35,6 +35,7 @@ import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase
import com.tangem.domain.swap.usecase.SelectInitialPairUseCase
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
import com.tangem.domain.transaction.usecase.GetAllowanceUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.v2.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener
@ -91,6 +92,7 @@ internal class SwapAmountModel @Inject constructor(
private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger,
private val shouldShowNotificationUseCase: ShouldShowNotificationUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getWalletsUseCase: GetWalletsUseCase,
) : Model(), SwapAmountClickIntents, SwapChooseProviderComponent.ModelCallback {
private val params: SwapAmountComponentParams = paramsContainer.require()
@ -105,10 +107,10 @@ internal class SwapAmountModel @Inject constructor(
private var secondaryMaximumAmountBoundary: EnterAmountBoundary? = null
private var secondaryMinimumAmountBoundary: EnterAmountBoundary? = null
var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country)
private var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country)
val bottomSheetNavigation: SlotNavigation<SwapChooseProviderConfig> = SlotNavigation()
var showBestRateAnimation: Boolean = false
private var showBestRateAnimation: Boolean = false
val uiState: StateFlow<SwapAmountUM>
field = MutableStateFlow(params.amountUM)
@ -131,10 +133,14 @@ internal class SwapAmountModel @Inject constructor(
subscribeOnAmountReduceByTriggerUpdates()
subscribeOnAmountIgnoreReduceTriggerUpdates()
subscribeOnReloadQuotesTriggerUpdates()
subscribeOnBalanceHiddenUpdates()
}
fun onStart() {
startLoadingQuotesTask(isSilentReload = false)
quoteTaskScheduler.scheduleTask(
scope = modelScope,
task = loadQuotesTask(),
)
}
fun onStop() {
@ -213,7 +219,11 @@ internal class SwapAmountModel @Inject constructor(
override fun onMaxValueClick() {
analyticsEventHandler.send(
CommonSendAmountAnalyticEvents.MaxAmountButtonClicked(categoryName = params.analyticsCategoryName),
CommonSendAmountAnalyticEvents.MaxAmountButtonClicked(
categoryName = params.analyticsCategoryName,
token = primaryCryptoCurrency.symbol,
blockchain = primaryCryptoCurrency.network.name,
),
)
uiState.transformerUpdate(
SwapAmountValueMaxTransformer(
@ -319,12 +329,29 @@ internal class SwapAmountModel @Inject constructor(
)
}
private fun subscribeOnBalanceHiddenUpdates() {
params.isBalanceHidingFlow.onEach { isHidden ->
val isSingleWallet = getWalletsUseCase.invokeSync().size == 1
uiState.transformerUpdate(
SwapAmountBalanceHiddenTransformer(
isBalanceHidden = isHidden,
isSingleWallet = isSingleWallet,
userWallet = userWallet,
appCurrency = appCurrency,
swapDirection = swapDirection,
clickIntents = this,
),
)
}.launchIn(modelScope)
}
private fun confirmSendWithSwapClose() {
val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return
val amountFieldData = uiState.value.primaryAmount.amountField as? AmountState.Data
val primaryCryptoCurrencyStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus
if (primaryCryptoCurrencyStatus != null) {
val isSingleWallet = getWalletsUseCase.invokeSync().size == 1
uiState.transformerUpdate(
SwapAmountPrimaryReadyStateTransformer(
userWallet = userWallet,
@ -334,6 +361,7 @@ internal class SwapAmountModel @Inject constructor(
clickIntents = this,
isBalanceHidden = params.isBalanceHidingFlow.value,
showBestRateAnimation = showBestRateAnimation,
isSingleWallet = isSingleWallet,
),
)
}
@ -359,6 +387,7 @@ internal class SwapAmountModel @Inject constructor(
),
)
} else {
val isSingleWallet = getWalletsUseCase.invokeSync().size == 1
uiState.transformerUpdate(
SwapAmountPrimaryReadyStateTransformer(
userWallet = userWallet,
@ -368,6 +397,7 @@ internal class SwapAmountModel @Inject constructor(
clickIntents = this,
isBalanceHidden = params.isBalanceHidingFlow.value,
showBestRateAnimation = showBestRateAnimation,
isSingleWallet = isSingleWallet,
),
)
}
@ -465,6 +495,7 @@ internal class SwapAmountModel @Inject constructor(
val primaryStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus
if (secondaryStatus != null && primaryStatus != null) {
initCurrencies(primaryStatus, secondaryStatus)
val isSingleWallet = getWalletsUseCase.invokeSync().size == 1
uiState.transformerUpdate(
SwapAmountSecondaryReadyStateTransformer(
userWallet = userWallet,
@ -476,6 +507,7 @@ internal class SwapAmountModel @Inject constructor(
clickIntents = this@SwapAmountModel,
isBalanceHidden = params.isBalanceHidingFlow.value,
showBestRateAnimation = showBestRateAnimation,
isSingleWallet = isSingleWallet,
),
)
startLoadingQuotesTask(isSilentReload = false)
@ -533,18 +565,17 @@ internal class SwapAmountModel @Inject constructor(
} as? AmountState.Data
val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value.orZero()
if (fromAmount?.amountTextField?.isError == true || fromAmountValue.isNullOrZero()) {
val isAmountScreen = params is SwapAmountComponentParams.AmountParams
val isAmountError = fromAmount?.amountTextField?.isError == true || fromAmountValue.isNullOrZero()
if (isAmountScreen && isAmountError) {
uiState.transformerUpdate(SwapQuoteEmptyStateTransformer)
return
}
val swapGroups = state.swapCurrencies.getGroupWithDirection(state.swapDirection)
uiState.transformerUpdate(SwapQuoteLoadingStateTransformer)
if (!isSilentReload) { uiState.transformerUpdate(SwapQuoteLoadingStateTransformer) }
modelScope.launch {
val quotes = swapGroups.available.filter {
val quotes = state.swapCurrencies.getGroupWithDirection(state.swapDirection).available.filter {
it.currencyStatus.currency.id == toCryptoCurrency.id
}.flatMap {
it.providers
@ -591,7 +622,6 @@ internal class SwapAmountModel @Inject constructor(
needApplyFcaRestrictions = userCountry.needApplyFCARestrictions(),
),
)
feeSelectorReloadTrigger.triggerUpdate()
}
}

View file

@ -25,6 +25,7 @@ internal class SwapAmountFieldConverter(
private val userWallet: UserWallet,
private val appCurrency: AppCurrency,
private val clickIntents: AmountScreenClickIntents,
private val isSingleWallet: Boolean,
) {
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
@ -48,13 +49,18 @@ internal class SwapAmountFieldConverter(
maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatus),
iconStateConverter = iconStateConverter,
isRedesignEnabled = true,
isBalanceHidden = isBalanceHidden,
).convert(
AmountParameters(
title = combinedReference(
resourceReference(R.string.send_from_wallet_android),
stringReference(" "),
stringReference(userWallet.name),
),
title = if (isSingleWallet) {
resourceReference(R.string.send_from_title)
} else {
combinedReference(
resourceReference(R.string.send_from_wallet_android),
stringReference(" "),
stringReference(userWallet.name),
)
},
value = "",
),
),

View file

@ -0,0 +1,66 @@
package com.tangem.features.swap.v2.impl.amount.model.transformers
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.swap.models.SwapDirection
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter
import com.tangem.utils.transformer.Transformer
internal class SwapAmountBalanceHiddenTransformer(
private val isBalanceHidden: Boolean,
private val isSingleWallet: Boolean,
private val userWallet: UserWallet,
private val appCurrency: AppCurrency,
private val swapDirection: SwapDirection,
private val clickIntents: AmountScreenClickIntents,
) : Transformer<SwapAmountUM> {
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
val content = prevState as? SwapAmountUM.Content ?: return prevState
val amountFieldConverter = SwapAmountFieldConverter(
swapDirection = swapDirection,
isBalanceHidden = isBalanceHidden,
userWallet = userWallet,
appCurrency = appCurrency,
clickIntents = clickIntents,
isSingleWallet = isSingleWallet,
)
val recalculatedPrimary = amountFieldConverter.convert(
selectedType = SwapAmountType.From,
cryptoCurrencyStatus = content.primaryCryptoCurrencyStatus,
) as SwapAmountFieldUM.Content
val oldPrimary = content.primaryAmount as? SwapAmountFieldUM.Content
val mergedAmountField = if (
oldPrimary?.amountField is AmountState.Data && recalculatedPrimary.amountField is AmountState.Data
) {
val oldData = oldPrimary.amountField
val newData = recalculatedPrimary.amountField
newData.copy(
amountTextField = oldData.amountTextField,
selectedButton = oldData.selectedButton,
isPrimaryButtonEnabled = oldData.isPrimaryButtonEnabled,
isSegmentedButtonsEnabled = oldData.isSegmentedButtonsEnabled,
isEditingDisabled = oldData.isEditingDisabled,
reduceAmountBy = oldData.reduceAmountBy,
isIgnoreReduce = oldData.isIgnoreReduce,
)
} else {
recalculatedPrimary.amountField
}
val updatedPrimaryAmount = recalculatedPrimary.copy(
amountField = mergedAmountField,
)
return content.copy(primaryAmount = updatedPrimaryAmount)
}
}

View file

@ -24,6 +24,7 @@ internal class SwapAmountPrimaryReadyStateTransformer(
private val swapDirection: SwapDirection,
private val isBalanceHidden: Boolean,
private val showBestRateAnimation: Boolean,
private val isSingleWallet: Boolean,
) : Transformer<SwapAmountUM> {
private val amountFieldConverter = SwapAmountFieldConverter(
@ -32,6 +33,7 @@ internal class SwapAmountPrimaryReadyStateTransformer(
userWallet = userWallet,
appCurrency = appCurrency,
clickIntents = clickIntents,
isSingleWallet = isSingleWallet,
)
override fun transform(prevState: SwapAmountUM): SwapAmountUM {

View file

@ -25,6 +25,7 @@ internal class SwapAmountSecondaryReadyStateTransformer(
private val swapDirection: SwapDirection,
private val isBalanceHidden: Boolean,
private val showBestRateAnimation: Boolean,
private val isSingleWallet: Boolean,
) : Transformer<SwapAmountUM> {
private val amountFieldConverter = SwapAmountFieldConverter(
@ -33,6 +34,7 @@ internal class SwapAmountSecondaryReadyStateTransformer(
userWallet = userWallet,
appCurrency = appCurrency,
clickIntents = clickIntents,
isSingleWallet = isSingleWallet,
)
override fun transform(prevState: SwapAmountUM): SwapAmountUM {

View file

@ -34,8 +34,10 @@ internal class SwapAmountSetQuotesTransformer(
val sortedQuotes = quotes.sortedWith(SwapQuotesComparator)
val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty
val quotesWithDiff = getQuotesWithDiff(sortedQuotes, bestQuote, isSingleProvider)
val selectedQuote = if (isSilentReload && prevState.selectedQuote !is SwapQuoteUM.Loading) {
prevState.selectedQuote
quotesWithDiff.firstOrNull { it.provider?.providerId == prevState.selectedQuote.provider?.providerId }
?: prevState.selectedQuote
} else {
(bestQuote as? SwapQuoteUM.Content)?.copy(
diffPercent = DifferencePercent.Best,
@ -55,7 +57,7 @@ internal class SwapAmountSetQuotesTransformer(
if (updatedState !is SwapAmountUM.Content) return prevState
return updatedState.copy(
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled && quotes.isNotEmpty(),
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled && quotesWithDiff.isNotEmpty(),
swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote, isSingleProvider),
)
}

View file

@ -45,6 +45,7 @@ internal class SwapAmountValueChangeTransformer(
)
return (updatedState as? SwapAmountUM.Content)?.copy(
isPrimaryButtonEnabled = false,
selectedQuote = if (updatedState.isPrimaryButtonEnabled) {
SwapQuoteUM.Empty
} else {

View file

@ -16,31 +16,34 @@ internal class SwapAmountValueMaxTransformer(
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
return prevState
.copy(selectedQuote = SwapQuoteUM.Loading)
.updateAmount(
onPrimaryAmount = { primaryStatus ->
val updatedState = prevState.updateAmount(
onPrimaryAmount = { primaryStatus ->
copy(
amountField = AmountFieldSetMaxAmountTransformer(
cryptoCurrencyStatus = primaryStatus,
maxAmount = primaryMaximumAmountBoundary,
minAmount = primaryMinimumAmountBoundary,
).transform(prevState.primaryAmount.amountField),
)
},
onSecondaryAmount = { secondaryStatus ->
if (secondaryMaximumAmountBoundary != null) {
copy(
amountField = AmountFieldSetMaxAmountTransformer(
cryptoCurrencyStatus = primaryStatus,
maxAmount = primaryMaximumAmountBoundary,
minAmount = primaryMinimumAmountBoundary,
).transform(prevState.primaryAmount.amountField),
cryptoCurrencyStatus = secondaryStatus,
maxAmount = secondaryMaximumAmountBoundary,
minAmount = secondaryMinimumAmountBoundary,
).transform(prevState.secondaryAmount.amountField),
)
},
onSecondaryAmount = { secondaryStatus ->
if (secondaryMaximumAmountBoundary != null) {
copy(
amountField = AmountFieldSetMaxAmountTransformer(
cryptoCurrencyStatus = secondaryStatus,
maxAmount = secondaryMaximumAmountBoundary,
minAmount = secondaryMinimumAmountBoundary,
).transform(prevState.secondaryAmount.amountField),
)
} else {
this
}
},
)
} else {
this
}
},
)
return (updatedState as? SwapAmountUM.Content)?.copy(
isPrimaryButtonEnabled = false,
selectedQuote = SwapQuoteUM.Loading,
) ?: updatedState
}
}

View file

@ -42,7 +42,7 @@ internal class SwapProviderStateConverter(
val additionalBadge = when {
needApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList
isNeedBestRateBadge && isBestRate && !needApplyFCARestrictions -> AdditionalBadge.BestTrade
isNeedBestRateBadge && isBestRate -> AdditionalBadge.BestTrade
else -> AdditionalBadge.Empty
}

View file

@ -94,13 +94,15 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor(
)
activeComponent.updateState(model.uiState.value.destinationUM)
}
is SendWithSwapConfirmComponent -> if (model.currentRoute.value.isEditMode) {
is SendWithSwapConfirmComponent -> {
analyticsEventHandler.send(
CommonSendAnalyticEvents.ConfirmationScreenOpened(
categoryName = model.analyticCategoryName,
),
)
activeComponent.updateState(model.uiState.value)
if (model.currentRoute.value.isEditMode) {
activeComponent.updateState(model.uiState.value)
}
}
}
model.currentRoute.emit(stack.active.configuration)

View file

@ -2,6 +2,7 @@ package com.tangem.features.swap.v2.impl.sendviaswap.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER
import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_TOKEN
@ -24,7 +25,7 @@ internal sealed class SendWithSwapAnalyticEvents(
event = "Send With Swap In Progress Screen Opened",
params = mapOf(
PROVIDER to providerName,
"Commission" to if (feeType is AnalyticsParam.FeeType.Normal) "Market" else "Fast",
FEE_TYPE to if (feeType is AnalyticsParam.FeeType.Normal) "Market" else "Fast",
SEND_TOKEN to fromToken.symbol,
RECEIVE_TOKEN to toToken.symbol,
SEND_BLOCKCHAIN to fromToken.network.name,

View file

@ -77,7 +77,7 @@ internal class DefaultTokenReceiveComponent @AssistedInject constructor(
is TokenReceiveRoutes.QrCode -> TokenReceiveQrCodeComponent(
appComponentContext = appComponentContext,
params = TokenReceiveQrCodeComponent.TokenReceiveQrCodeParams(
network = model.params.config.cryptoCurrency.network.name,
cryptoCurrency = model.params.config.cryptoCurrency,
address = model.state.value.addresses[config.addressId] ?: error("Address has to be there"),
callback = model,
onDismiss = ::dismiss,

View file

@ -8,6 +8,7 @@ import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.tokens.model.analytics.TokenReceiveCopyActionSource
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.model.TokenReceiveAssetsModel
import com.tangem.features.tokenreceive.ui.TokenReceiveAssetsContent
@ -29,7 +30,7 @@ internal class TokenReceiveAssetsComponent(
internal interface TokenReceiveAssetsModelCallback {
fun onQrCodeClick(id: Int)
fun onCopyClick(id: Int)
fun onCopyClick(id: Int, source: TokenReceiveCopyActionSource)
}
data class TokenReceiveAssetsParams(

View file

@ -7,6 +7,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.analytics.TokenReceiveCopyActionSource
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.model.TokenReceiveQrCodeModel
import com.tangem.features.tokenreceive.ui.TokenReceiveQrCodeContent
@ -25,13 +27,13 @@ internal class TokenReceiveQrCodeComponent(
}
internal interface TokenReceiveQrCodeModelCallback {
fun onCopyClick(id: Int)
fun onCopyClick(id: Int, source: TokenReceiveCopyActionSource)
fun onShareClick(address: String)
}
data class TokenReceiveQrCodeParams(
val id: Int,
val network: String,
val cryptoCurrency: CryptoCurrency,
val address: ReceiveAddress,
val callback: TokenReceiveQrCodeModelCallback,
val onDismiss: () -> Unit,

View file

@ -1,16 +1,24 @@
package com.tangem.features.tokenreceive.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
internal data class ReceiveAddress(
val value: String,
val type: Type,
) {
@Immutable
sealed interface Type {
data object Ens : Type
data class Default(
val displayName: TextReference,
) : Type
sealed interface Primary : Type {
val displayName: TextReference
data class Default(override val displayName: TextReference) : Primary
data class Legacy(override val displayName: TextReference) : Primary
}
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.tokens.model.analytics.TokenReceiveCopyActionSource
import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent
import com.tangem.features.tokenreceive.component.TokenReceiveAssetsComponent
import com.tangem.features.tokenreceive.entity.ReceiveAddress
@ -38,13 +39,17 @@ internal class TokenReceiveAssetsModel @Inject constructor(
internal val state: StateFlow<ReceiveAssetsUM>
field = MutableStateFlow<ReceiveAssetsUM>(
ReceiveAssetsUM(
onCopyClick = params.callback::onCopyClick,
onCopyClick = {
params.callback.onCopyClick(
id = it,
source = TokenReceiveCopyActionSource.Receive,
)
},
onOpenQrCodeClick = params.callback::onQrCodeClick,
addresses = params.addresses,
showMemoDisclaimer = params.showMemoDisclaimer,
isEnsResultLoading = false,
notificationConfigs = params.notificationConfigs,
fullName = params.fullName,
),
)

View file

@ -12,19 +12,22 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.models.Asset
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.ens.EnsAddress
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.SaveViewedTokenReceiveWarningUseCase
import com.tangem.domain.tokens.model.analytics.TokenReceiveCopyActionSource
import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent
import com.tangem.domain.transaction.usecase.GetReverseResolvedEnsAddressUseCase
import com.tangem.features.tokenreceive.TokenReceiveComponent
import com.tangem.features.tokenreceive.component.TokenReceiveModelCallback
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.entity.ReceiveAddress.Type.Ens
import com.tangem.features.tokenreceive.entity.ReceiveAddress.Type.Primary
import com.tangem.features.tokenreceive.route.TokenReceiveRoutes
import com.tangem.features.tokenreceive.ui.state.TokenReceiveUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -74,9 +77,9 @@ internal class TokenReceiveModel @Inject constructor(
stackNavigation.push(configuration = TokenReceiveRoutes.QrCode(addressId = id))
}
override fun onCopyClick(id: Int) {
override fun onCopyClick(id: Int, source: TokenReceiveCopyActionSource) {
val addressToCopy = state.value.addresses[id] ?: return
sendCopyActionAnalytic(addressToCopy)
sendCopyActionAnalytic(addressToCopy, source)
clipboardManager.setText(text = addressToCopy.value, isSensitive = true)
}
@ -103,17 +106,35 @@ internal class TokenReceiveModel @Inject constructor(
}
}
private fun mapAddresses(addresses: List<ReceiveAddressModel>): ImmutableMap<Int, ReceiveAddress> {
private fun mapAddresses(
addresses: List<ReceiveAddressModel>,
networkName: String,
): ImmutableMap<Int, ReceiveAddress> {
val needUseToLegacyAndDefaultName = addresses.any { it.nameService == ReceiveAddressModel.NameService.Legacy }
return buildMap {
addresses.mapIndexed { index, model ->
val type = when (model.nameService) {
ReceiveAddressModel.NameService.Default -> {
ReceiveAddress.Type.Default(
displayName = stringReference(model.displayName),
Primary.Default(
displayName = if (needUseToLegacyAndDefaultName) {
TextReference.Res(R.string.domain_receive_assets_default_address)
} else {
TextReference.Combined(
wrappedList(
TextReference.Str(networkName),
TextReference.Str(" "),
TextReference.Res(R.string.common_address),
),
)
},
)
}
ReceiveAddressModel.NameService.Ens -> ReceiveAddress.Type.Ens
ReceiveAddressModel.NameService.Ens -> Ens
ReceiveAddressModel.NameService.Legacy -> Primary.Legacy(
displayName = TextReference.Res(R.string.domain_receive_assets_legacy_address),
)
}
put(
key = index,
value = ReceiveAddress(
@ -146,10 +167,18 @@ internal class TokenReceiveModel @Inject constructor(
val newEnsAddresses = reverseResolveResult
.filterIsInstance<EnsAddress.Address>()
.filterNot { it.name in currentAddressValues }
.map { ensAddress -> ReceiveAddress(value = ensAddress.name, type = ReceiveAddress.Type.Ens) }
.map { ensAddress -> ReceiveAddress(value = ensAddress.name, type = Ens) }
val combinedAddresses = (state.value.addresses.values + newEnsAddresses)
.sortedWith(compareByDescending { it.type is ReceiveAddress.Type.Ens })
.sortedWith(
compareBy { address ->
when (address.type) {
is Ens -> 0
is Primary.Default -> 1
is Primary.Legacy -> 2
}
},
)
val updatedAddresses = combinedAddresses
.mapIndexed { index, address -> index to address }
@ -191,7 +220,10 @@ internal class TokenReceiveModel @Inject constructor(
private fun getInitState(): TokenReceiveUM {
return TokenReceiveUM(
addresses = mapAddresses(params.config.receiveAddress),
addresses = mapAddresses(
addresses = params.config.receiveAddress,
networkName = params.config.cryptoCurrency.network.name,
),
iconState = iconStateConverter.convert(params.config.cryptoCurrency),
network = params.config.cryptoCurrency.network.name,
isEnsResultLoading = false,
@ -199,15 +231,16 @@ internal class TokenReceiveModel @Inject constructor(
)
}
private fun sendCopyActionAnalytic(receiveAddress: ReceiveAddress) {
private fun sendCopyActionAnalytic(receiveAddress: ReceiveAddress, source: TokenReceiveCopyActionSource) {
val event = when (receiveAddress.type) {
is ReceiveAddress.Type.Default -> {
is Primary -> {
TokenReceiveNewAnalyticsEvent.ButtonCopyAddress(
token = getTokenName(),
blockchainName = params.config.cryptoCurrency.network.name,
tokenReceiveSource = source,
)
}
ReceiveAddress.Type.Ens -> {
Ens -> {
TokenReceiveNewAnalyticsEvent.ButtonCopyEns(
token = getTokenName(),
blockchainName = params.config.cryptoCurrency.network.name,

View file

@ -5,8 +5,8 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.tokens.model.analytics.TokenReceiveCopyActionSource
import com.tangem.features.tokenreceive.component.TokenReceiveQrCodeComponent
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.ui.state.QrCodeUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
@ -25,10 +25,15 @@ internal class TokenReceiveQrCodeModel @Inject constructor(
internal val state: StateFlow<QrCodeUM>
field = MutableStateFlow<QrCodeUM>(
QrCodeUM(
network = params.network,
network = params.cryptoCurrency.network.name,
addressValue = params.address.value,
addressName = (params.address.type as? ReceiveAddress.Type.Default)?.displayName ?: TextReference.EMPTY,
onCopyClick = { params.callback.onCopyClick(params.id) },
addressName = TextReference.Str("${params.cryptoCurrency.name} (${params.cryptoCurrency.symbol})"),
onCopyClick = {
params.callback.onCopyClick(
id = params.id,
source = TokenReceiveCopyActionSource.QR,
)
},
onShareClick = params.callback::onShareClick,
),
)

View file

@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.iconResId
import com.tangem.features.tokenreceive.component.TokenReceiveWarningComponent
import com.tangem.features.tokenreceive.ui.state.WarningUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -27,7 +26,6 @@ internal class TokenReceiveWarningModel @Inject constructor(
iconState = params.iconState,
onWarningAcknowledged = params.callback::onWarningAcknowledged,
network = params.network.name,
networkIcon = params.network.iconResId,
),
)
}

View file

@ -36,6 +36,7 @@ import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.buttons.small.TangemIconButton
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.components.notifications.Notification
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.extensions.stringResourceSafe
@ -78,7 +79,6 @@ internal fun TokenReceiveAssetsContent(assetsUM: ReceiveAssetsUM) {
onOpenQrCodeClick = assetsUM.onOpenQrCodeClick,
addresses = assetsUM.addresses,
snackbarHostState = snackbarHostState,
fullName = assetsUM.fullName,
)
if (assetsUM.isEnsResultLoading) {
@ -125,7 +125,6 @@ private fun AddressBlock(
onCopyClick: (id: Int) -> Unit,
addresses: ImmutableMap<Int, ReceiveAddress>,
snackbarHostState: SnackbarHostState,
fullName: String,
) {
val hapticFeedback = LocalHapticFeedback.current
val coroutineScope = rememberCoroutineScope()
@ -133,8 +132,8 @@ private fun AddressBlock(
val resources = context.resources
addresses.entries.toList().fastForEach { entry ->
when (entry.value.type) {
is ReceiveAddress.Type.Default -> {
when (val type = entry.value.type) {
is ReceiveAddress.Type.Primary -> {
key(entry.key) {
AddressItem(
onCopyClick = {
@ -152,8 +151,8 @@ private fun AddressBlock(
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onOpenQrCodeClick(entry.key)
},
fullName = fullName,
address = entry.value.value,
primaryType = type,
)
SpacerH8()
}
@ -184,9 +183,9 @@ private fun AddressBlock(
@Composable
private fun AddressItem(
onOpenQrCodeClick: () -> Unit,
fullName: String,
onCopyClick: () -> Unit,
address: String,
primaryType: ReceiveAddress.Type.Primary,
modifier: Modifier = Modifier,
) {
Card(
@ -195,52 +194,56 @@ private fun AddressItem(
colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action),
onClick = onOpenQrCodeClick,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 14.dp, horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
Column(
modifier = Modifier.padding(vertical = 14.dp, horizontal = 12.dp),
) {
IdentIcon(
address = address,
modifier = Modifier
.size(size = 36.dp)
.clip(shape = RoundedCornerShape(18.dp)),
)
SpacerW12()
Column(modifier = Modifier.weight(1f)) {
EllipsisText(
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_network_name, fullName),
ellipsis = TextEllipsis.Middle,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
IdentIcon(
address = address,
modifier = Modifier
.size(size = 36.dp)
.clip(shape = RoundedCornerShape(18.dp)),
)
EllipsisText(
text = address,
ellipsis = TextEllipsis.Middle,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
SpacerW12()
Column(modifier = Modifier.weight(1f)) {
EllipsisText(
text = primaryType.displayName.resolveReference(),
ellipsis = TextEllipsis.Middle,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
)
EllipsisText(
text = address,
ellipsis = TextEllipsis.Middle,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
)
}
SpacerW12()
TangemIconButton(
modifier = Modifier.size(TangemTheme.dimens.size28),
innerPadding = 6.dp,
iconRes = R.drawable.ic_qrcode_new_24,
onClick = onOpenQrCodeClick,
)
SpacerW8()
TangemIconButton(
modifier = Modifier.size(TangemTheme.dimens.size28),
iconRes = R.drawable.ic_copy_new_24,
innerPadding = 6.dp,
onClick = onCopyClick,
)
}
SpacerW12()
TangemIconButton(
modifier = Modifier.size(TangemTheme.dimens.size28),
iconRes = R.drawable.ic_qrcode_new_24,
onClick = onOpenQrCodeClick,
)
SpacerW8()
TangemIconButton(
modifier = Modifier.size(TangemTheme.dimens.size28),
iconRes = R.drawable.ic_copy_new_24,
onClick = onCopyClick,
)
}
}
}
@ -275,8 +278,9 @@ private fun EnsItem(onCopyClick: () -> Unit, address: String, modifier: Modifier
)
TangemIconButton(
modifier = Modifier.size(28.dp),
modifier = Modifier.size(TangemTheme.dimens.size28),
iconRes = R.drawable.ic_copy_new_24,
innerPadding = 6.dp,
onClick = onCopyClick,
)
}
@ -318,7 +322,7 @@ private fun Preview_TokenReceiveAssetsContent(
private class TokenReceiveAssetsContentProvider : PreviewParameterProvider<ReceiveAssetsUM> {
val address = ReceiveAddress(
value = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d",
type = ReceiveAddress.Type.Default(
type = ReceiveAddress.Type.Primary.Default(
displayName = stringReference("Etherium address"),
),
)
@ -338,7 +342,6 @@ private class TokenReceiveAssetsContentProvider : PreviewParameterProvider<Recei
onCopyClick = {},
onOpenQrCodeClick = {},
isEnsResultLoading = true,
fullName = "Etherium",
)
override val values: Sequence<ReceiveAssetsUM>

View file

@ -1,6 +1,7 @@
package com.tangem.features.tokenreceive.ui
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -83,7 +84,7 @@ internal fun TokenReceiveContent(
) {
Children(
stack = stackState,
animation = stackAnimation(fade()),
animation = stackAnimation(fade(animationSpec = tween(durationMillis = 100))),
modifier = modifier
.fillMaxSize()
.animateContentSize(),

View file

@ -3,15 +3,12 @@ package com.tangem.features.tokenreceive.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
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.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -21,6 +18,7 @@ import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerW6
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.stringResourceSafe
@ -46,15 +44,17 @@ internal fun TokenReceiveWarningContent(warningUM: WarningUM) {
horizontalAlignment = Alignment.CenterHorizontally,
) {
CurrencyIcon(
modifier = Modifier.size(size = 56.dp),
modifier = Modifier
.padding(8.dp)
.size(size = 64.dp),
state = warningUM.iconState,
shouldDisplayNetwork = false,
shouldDisplayNetwork = true,
iconSize = 56.dp,
)
SpacerH24()
WarningBlock(networkIcon = warningUM.networkIcon, networkName = warningUM.network)
WarningBlock(networkName = warningUM.network)
SpacerH12()
@ -79,7 +79,7 @@ internal fun TokenReceiveWarningContent(warningUM: WarningUM) {
}
@Composable
fun WarningBlock(networkName: String, networkIcon: Int, modifier: Modifier = Modifier) {
fun WarningBlock(networkName: String, modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
@ -91,21 +91,14 @@ fun WarningBlock(networkName: String, networkIcon: Int, modifier: Modifier = Mod
color = TangemTheme.colors.text.primary1,
)
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
modifier = Modifier.size(20.dp),
painter = painterResource(id = networkIcon),
tint = Color.Unspecified,
contentDescription = null,
)
SpacerW6()
Text(
textAlign = TextAlign.Center,
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_network_name, networkName),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
}
Text(
textAlign = TextAlign.Center,
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_network_name, networkName),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
}
}
@ -136,7 +129,6 @@ private class TokenReceiveWarningContentProvider : PreviewParameterProvider<Warn
iconState = iconState,
onWarningAcknowledged = {},
network = "Etherium",
networkIcon = R.drawable.ic_eth_16,
),
)
}

View file

@ -12,5 +12,4 @@ internal data class ReceiveAssetsUM(
val onCopyClick: (id: Int) -> Unit,
val isEnsResultLoading: Boolean,
val notificationConfigs: ImmutableList<NotificationUM>,
val fullName: String,
)

View file

@ -4,7 +4,6 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState
internal data class WarningUM(
val network: String,
val networkIcon: Int,
val iconState: CurrencyIconState,
val onWarningAcknowledged: () -> Unit,
)

View file

@ -58,6 +58,8 @@ import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenReceiveCopyActionSource
import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent
@ -876,7 +878,13 @@ internal class TokenDetailsModel @Inject constructor(
vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click)
clipboardManager.setText(text = defaultAddress, isSensitive = true)
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol))
analyticsEventsHandler.send(
TokenReceiveNewAnalyticsEvent.ButtonCopyAddress(
token = cryptoCurrency.symbol,
blockchainName = cryptoCurrency.network.name,
tokenReceiveSource = TokenReceiveCopyActionSource.Token,
),
)
return resourceReference(R.string.wallet_notification_address_copied)
}
@ -1089,16 +1097,17 @@ internal class TokenDetailsModel @Inject constructor(
ReceiveAddressModel(
nameService = ReceiveAddressModel.NameService.Ens,
value = ens,
displayName = ens,
),
)
}
addresses.availableAddresses.map { address ->
add(
ReceiveAddressModel(
nameService = ReceiveAddressModel.NameService.Default,
nameService = when (address.type) {
NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.NameService.Default
NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.NameService.Legacy
},
value = address.value,
displayName = "${cryptoCurrency.name} (${cryptoCurrency.symbol})",
),
)
}

View file

@ -49,6 +49,8 @@ import com.tangem.domain.tokens.RemoveCurrencyUseCase
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenReceiveCopyActionSource
import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.AVAILABLE
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText
@ -174,10 +176,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
),
)
analyticsEventHandler.send(
event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrencyStatus.currency.symbol),
)
event?.let { analyticsEventHandler.send(it) }
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
@ -188,6 +186,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
}
}
} else {
analyticsEventHandler.send(
event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrencyStatus.currency.symbol),
)
stateHolder.showBottomSheet(
createReceiveBottomSheetContent(
currency = cryptoCurrencyStatus.currency,
@ -206,7 +207,13 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click)
clipboardManager.setText(text = defaultAddress, isSensitive = true)
analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol))
analyticsEventHandler.send(
TokenReceiveNewAnalyticsEvent.ButtonCopyAddress(
token = cryptoCurrency.symbol,
blockchainName = cryptoCurrency.network.name,
tokenReceiveSource = TokenReceiveCopyActionSource.Main,
),
)
return resourceReference(R.string.wallet_notification_address_copied)
}
@ -235,7 +242,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
override fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
analyticsEventHandler.send(
event = TokenScreenAnalyticsEvent.ButtonCopyAddress(cryptoCurrencyStatus.currency.symbol),
event = TokenReceiveNewAnalyticsEvent.ButtonCopyAddress(
token = cryptoCurrencyStatus.currency.symbol,
blockchainName = cryptoCurrencyStatus.currency.network.name,
tokenReceiveSource = TokenReceiveCopyActionSource.Main,
),
)
modelScope.launch(dispatchers.main) {
@ -673,16 +684,17 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
ReceiveAddressModel(
nameService = ReceiveAddressModel.NameService.Ens,
value = ens,
displayName = ens,
),
)
}
networkAddress.availableAddresses.map { address ->
add(
ReceiveAddressModel(
nameService = ReceiveAddressModel.NameService.Default,
nameService = when (address.type) {
NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.NameService.Default
NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.NameService.Legacy
},
value = address.value,
displayName = "${cryptoCurrencyStatus.currency.name} (${cryptoCurrencyStatus.currency.symbol})",
),
)
}

View file

@ -1,3 +1,5 @@
@file:Suppress("FunctionSignature")
package com.tangem.feature.wallet.presentation.wallet.deeplink
import arrow.core.Either
@ -354,80 +356,82 @@ class DefaultPromoDeeplinkHandlerTest {
}
@Test
fun `GIVEN BTC status but currencies without BTC WHEN findBitcoinAddress THEN no bitcoin address dialog is shown`() = runTest {
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to "PROMO123")
val userWallet = mockUserWallet("ABCDEF")
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
val btcStatus = buildNetworkStatus(rawNetworkId = Blockchain.Bitcoin.id, address = "bc1qxyz")
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatus))
val ethOnly = buildCryptoCurrency(rawNetworkId = "ethereum")
coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(ethOnly)
val dispatcherProvider = testDispatcherProvider(testScheduler)
fun `GIVEN BTC status but currencies without BTC WHEN findBitcoinAddress THEN no bitcoin address dialog is shown`() =
runTest {
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to "PROMO123")
val userWallet = mockUserWallet("ABCDEF")
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
val btcStatus = buildNetworkStatus(rawNetworkId = Blockchain.Bitcoin.id, address = "bc1qxyz")
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatus))
val ethOnly = buildCryptoCurrency(rawNetworkId = "ethereum")
coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(ethOnly)
val dispatcherProvider = testDispatcherProvider(testScheduler)
DefaultPromoDeeplinkHandler(
scope = this,
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
DefaultPromoDeeplinkHandler(
scope = this,
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
advanceUntilIdle()
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
}
val sent = messages.last { it is DialogMessage } as DialogMessage
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
}
@Test
fun `GIVEN multiple statuses emissions and currencies without BTC WHEN findBitcoinAddress THEN no bitcoin address dialog is shown`() = runTest {
val promoCode = "PROMO123"
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode)
val userWallet = mockUserWallet("ABCDEF")
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
fun `GIVEN multiple statuses emissions and currencies without BTC WHEN findBitcoinAddress THEN no bitcoin address dialog is shown`() =
runTest {
val promoCode = "PROMO123"
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode)
val userWallet = mockUserWallet("ABCDEF")
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
val ethStatus = buildNetworkStatus(rawNetworkId = "ethereum", address = "0x123")
val btcStatus = buildNetworkStatus(rawNetworkId = Blockchain.Bitcoin.id, address = "bc1qxyz")
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flow {
emit(emptySet())
emit(setOf(ethStatus))
emit(setOf(ethStatus, btcStatus))
}
val ethStatus = buildNetworkStatus(rawNetworkId = "ethereum", address = "0x123")
val btcStatus = buildNetworkStatus(rawNetworkId = Blockchain.Bitcoin.id, address = "bc1qxyz")
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flow {
emit(emptySet())
emit(setOf(ethStatus))
emit(setOf(ethStatus, btcStatus))
}
val ethOnly = buildCryptoCurrency(rawNetworkId = "ethereum")
coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(ethOnly)
val ethOnly = buildCryptoCurrency(rawNetworkId = "ethereum")
coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(ethOnly)
val dispatcherProvider = testDispatcherProvider(testScheduler)
val dispatcherProvider = testDispatcherProvider(testScheduler)
DefaultPromoDeeplinkHandler(
scope = this,
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
DefaultPromoDeeplinkHandler(
scope = this,
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
)
}
}
}
@Test
fun `GIVEN two BTC statuses with different derivation AND two BTC currencies WHEN activate THEN activated`() =
@ -485,270 +489,275 @@ class DefaultPromoDeeplinkHandlerTest {
}
@Test
fun `GIVEN two BTC statuses with different derivation AND one matching BTC currency WHEN activate THEN activated`() = runTest {
val promoCode = "PROMO123"
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode)
val userWallet = mockUserWallet("ABCDEF")
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
fun `GIVEN two BTC statuses with different derivation AND one matching BTC currency WHEN activate THEN activated`() =
runTest {
val promoCode = "PROMO123"
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode)
val userWallet = mockUserWallet("ABCDEF")
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
val dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
val dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
val btcStatusCard = buildNetworkStatus(
rawNetworkId = Blockchain.Bitcoin.id,
address = "bc1qcard",
derivationPath = dpCard,
)
val btcStatusCustom = buildNetworkStatus(
rawNetworkId = Blockchain.Bitcoin.id,
address = "bc1qcustom",
derivationPath = dpCustom,
)
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCard, btcStatusCustom))
val btcCoinCard = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCard)
coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoinCard)
coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qcard", promoCode) } returns Either.Right("ok")
val dispatcherProvider = testDispatcherProvider(testScheduler)
DefaultPromoDeeplinkHandler(
scope = this,
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success))
coVerify(exactly = 1) { activateBitcoinPromocodeUseCase.invoke("bc1qcard", promoCode) }
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke("bc1qcustom", promoCode) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Activated),
val btcStatusCard = buildNetworkStatus(
rawNetworkId = Blockchain.Bitcoin.id,
address = "bc1qcard",
derivationPath = dpCard,
)
val btcStatusCustom = buildNetworkStatus(
rawNetworkId = Blockchain.Bitcoin.id,
address = "bc1qcustom",
derivationPath = dpCustom,
)
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCard, btcStatusCustom))
val btcCoinCard = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCard)
coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoinCard)
coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qcard", promoCode) } returns Either.Right("ok")
val dispatcherProvider = testDispatcherProvider(testScheduler)
DefaultPromoDeeplinkHandler(
scope = this,
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success))
coVerify(exactly = 1) { activateBitcoinPromocodeUseCase.invoke("bc1qcard", promoCode) }
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke("bc1qcustom", promoCode) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.Activated),
)
}
}
}
@Test
fun `GIVEN two BTC statuses with different derivation AND no BTC currencies WHEN findBitcoinAddress THEN no bitcoin address dialog is shown`() = runTest {
val promoCode = "PROMO123"
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode)
val userWallet = mockUserWallet("ABCDEF")
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
fun `GIVEN two BTC statuses with different derivation AND no BTC currencies WHEN findBitcoinAddress THEN no bitcoin address dialog is shown`() =
runTest {
val promoCode = "PROMO123"
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode)
val userWallet = mockUserWallet("ABCDEF")
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
val dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
val dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
val btcStatusCard = buildNetworkStatus(
rawNetworkId = Blockchain.Bitcoin.id,
address = "bc1qcard",
derivationPath = dpCard,
)
val btcStatusCustom = buildNetworkStatus(
rawNetworkId = Blockchain.Bitcoin.id,
address = "bc1qcustom",
derivationPath = dpCustom,
)
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCard, btcStatusCustom))
coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns emptySet()
val dispatcherProvider = testDispatcherProvider(testScheduler)
DefaultPromoDeeplinkHandler(
scope = this,
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
val btcStatusCard = buildNetworkStatus(
rawNetworkId = Blockchain.Bitcoin.id,
address = "bc1qcard",
derivationPath = dpCard,
)
val btcStatusCustom = buildNetworkStatus(
rawNetworkId = Blockchain.Bitcoin.id,
address = "bc1qcustom",
derivationPath = dpCustom,
)
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCard, btcStatusCustom))
coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns emptySet()
val dispatcherProvider = testDispatcherProvider(testScheduler)
DefaultPromoDeeplinkHandler(
scope = this,
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
)
}
}
}
@Test
fun `GIVEN two BTC currencies first derivation mismatched AND one matching status WHEN findBitcoinAddress THEN no bitcoin address dialog is shown`() = runTest {
val promoCode = "PROMO123"
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode)
val userWallet = mockUserWallet("ABCDEF")
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
fun `GIVEN two BTC currencies first derivation mismatched AND one matching status WHEN findBitcoinAddress THEN no bitcoin address dialog is shown`() =
runTest {
val promoCode = "PROMO123"
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode)
val userWallet = mockUserWallet("ABCDEF")
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
val dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
val dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
val btcStatusCard = buildNetworkStatus(
rawNetworkId = Blockchain.Bitcoin.id,
address = "bc1qcard",
derivationPath = dpCard,
)
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCard))
val btcCoinCustom = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCustom)
val btcCoinCard = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCard)
coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(
btcCoinCustom,
btcCoinCard,
)
val dispatcherProvider = testDispatcherProvider(testScheduler)
DefaultPromoDeeplinkHandler(
scope = this,
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
val btcStatusCard = buildNetworkStatus(
rawNetworkId = Blockchain.Bitcoin.id,
address = "bc1qcard",
derivationPath = dpCard,
)
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCard))
val btcCoinCustom = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCustom)
val btcCoinCard = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCard)
coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(
btcCoinCustom,
btcCoinCard,
)
val dispatcherProvider = testDispatcherProvider(testScheduler)
DefaultPromoDeeplinkHandler(
scope = this,
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
)
}
}
}
@Test
fun `GIVEN only BTC status with CUSTOM derivation AND only BTC currency with CARD derivation WHEN findBitcoinAddress THEN no bitcoin address dialog is shown`() = runTest {
val promoCode = "PROMO123"
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode)
val userWallet = mockUserWallet("ABCDEF")
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
fun `GIVEN only BTC status with CUSTOM derivation AND only BTC currency with CARD derivation WHEN findBitcoinAddress THEN no bitcoin address dialog is shown`() =
runTest {
val promoCode = "PROMO123"
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode)
val userWallet = mockUserWallet("ABCDEF")
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
val dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
val dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
val btcStatusCustom = buildNetworkStatus(
rawNetworkId = Blockchain.Bitcoin.id,
address = "bc1qcustom",
derivationPath = dpCustom,
)
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCustom))
val btcCoinCard = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCard)
coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoinCard)
val dispatcherProvider = testDispatcherProvider(testScheduler)
DefaultPromoDeeplinkHandler(
scope = this,
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
val btcStatusCustom = buildNetworkStatus(
rawNetworkId = Blockchain.Bitcoin.id,
address = "bc1qcustom",
derivationPath = dpCustom,
)
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCustom))
val btcCoinCard = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCard)
coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoinCard)
val dispatcherProvider = testDispatcherProvider(testScheduler)
DefaultPromoDeeplinkHandler(
scope = this,
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
)
}
}
}
@Test
fun `GIVEN only BTC status with CARD derivation AND only BTC currency with CUSTOM derivation WHEN findBitcoinAddress THEN no bitcoin address dialog is shown`() = runTest {
val promoCode = "PROMO123"
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode)
val userWallet = mockUserWallet("ABCDEF")
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
fun `GIVEN only BTC status with CARD derivation AND only BTC currency with CUSTOM derivation WHEN findBitcoinAddress THEN no bitcoin address dialog is shown`() =
runTest {
val promoCode = "PROMO123"
val queryParams = mapOf(DeeplinkConst.PROMO_CODE_KEY to promoCode)
val userWallet = mockUserWallet("ABCDEF")
every { getSelectedWalletSyncUseCase.invoke() } returns Either.Right(userWallet)
val dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
val dpCard = Network.DerivationPath.Card("m/44'/0'/0'")
val dpCustom = Network.DerivationPath.Custom("m/84'/0'/0'")
val btcStatusCard = buildNetworkStatus(
rawNetworkId = Blockchain.Bitcoin.id,
address = "bc1qcard",
derivationPath = dpCard,
)
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCard))
val btcCoinCustom = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCustom)
coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoinCustom)
val dispatcherProvider = testDispatcherProvider(testScheduler)
DefaultPromoDeeplinkHandler(
scope = this,
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
val btcStatusCard = buildNetworkStatus(
rawNetworkId = Blockchain.Bitcoin.id,
address = "bc1qcard",
derivationPath = dpCard,
)
coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatusCard))
val btcCoinCustom = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCustom)
coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoinCustom)
val dispatcherProvider = testDispatcherProvider(testScheduler)
DefaultPromoDeeplinkHandler(
scope = this,
queryParams = queryParams,
uiMessageSender = uiMessageSender,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
activateBitcoinPromocodeUseCase = activateBitcoinPromocodeUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
analyticsEventsHandler = analyticsEventHandler,
dispatchers = dispatcherProvider,
)
advanceUntilIdle()
val sent = messages.last { it is DialogMessage } as DialogMessage
Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title))
Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address))
coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) }
verify(exactly = 1) { analyticsEventHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart) }
verify(exactly = 1) {
analyticsEventHandler.send(
PromoActivationAnalytics.PromoActivation(PromoCodeActivationResult.NoBitcoinAddress),
)
}
}
}
private fun mockUserWallet(id: String): UserWallet {
val userWallet = mockk<UserWallet>(relaxed = true)

View file

@ -5,9 +5,9 @@
# https://github.com/tangem/tangem-sdk-android/
# https://github.com/tangem/vico
tangemBlockchainSdk = "develop-1205"
tangemBlockchainSdk = "develop-1212"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-557"
tangemCardSdk = "develop-560"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
tangemVico = "2.0.0-alpha.25-tangem12"
#tangemVico = "0.0.1" # Keep it! - used for local builds ^

View file

@ -165,8 +165,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
"zklink/test" -> Blockchain.ZkLinkNovaTestnet
"pepecoin" -> Blockchain.Pepecoin
"pepecoin/test" -> Blockchain.PepecoinTestnet
"hyperliquid" -> Blockchain.Hyperliquid
"hyperliquid/test" -> Blockchain.HyperliquidTestnet
"hyperevm" -> Blockchain.Hyperliquid
"hyperevm/test" -> Blockchain.HyperliquidTestnet
else -> null
}
}
@ -329,8 +329,8 @@ fun Blockchain.toNetworkId(): String {
Blockchain.ZkLinkNovaTestnet -> "zklink/test"
Blockchain.Pepecoin -> "pepecoin"
Blockchain.PepecoinTestnet -> "pepecoin/test"
Blockchain.Hyperliquid -> "hyperliquid"
Blockchain.HyperliquidTestnet -> "hyperliquid/test"
Blockchain.Hyperliquid -> "hyperevm"
Blockchain.HyperliquidTestnet -> "hyperevm/test"
}
}