Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-03 17:57:00 +07:00
commit f3fdcb3e3f
97 changed files with 1627 additions and 324 deletions

@ -1 +1 @@
Subproject commit 8285f8d2896d2994337f55a7b3a54fd5acc24304
Subproject commit b55ca9ec9bc7ff88a8764f00656ee7a884df9125

View file

@ -393,4 +393,18 @@ internal object TransactionDomainModule {
currencyChecksRepository = currencyChecksRepository,
)
}
@Provides
@Singleton
fun provideSignCloreMessageUseCase(
walletManagersFacade: WalletManagersFacade,
cardSdkConfigRepository: CardSdkConfigRepository,
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
): SignCloreMessageUseCase {
return SignCloreMessageUseCase(
walletManagersFacade = walletManagersFacade,
cardSdkConfigRepository = cardSdkConfigRepository,
getHotWalletSigner = tangemHotWalletSignerFactory::create,
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -85,10 +86,12 @@ internal object WalletsDomainModule {
fun providesSaveWalletUseCase(
userWalletsListRepository: UserWalletsListRepository,
walletsRepository: WalletsRepository,
analyticsEventHandler: AnalyticsEventHandler,
): SaveWalletUseCase {
return SaveWalletUseCase(
userWalletsListRepository = userWalletsListRepository,
walletsRepository = walletsRepository,
analyticsEventHandler = analyticsEventHandler,
)
}

View file

@ -21,6 +21,7 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.wallets.R
import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
@ -390,6 +391,7 @@ internal class DefaultUserWalletsListRepository(
if (newUserWallet.walletId == oldUserWallet.walletId &&
oldUserWallet is UserWallet.Hot && newUserWallet is UserWallet.Cold
) {
trackWalletUpgradeEvent()
removeHotWalletsFromSDKAndRepos(walletIds = listOf(oldUserWallet.walletId))
// When upgrading from Hot to Cold, if biometric lock is available, set it
if (hasBiometry()) {
@ -536,4 +538,8 @@ internal class DefaultUserWalletsListRepository(
),
)
}
private fun trackWalletUpgradeEvent() {
analyticsEventHandler.send(event = WalletSettingsAnalyticEvents.WalletUpgraded())
}
}

View file

@ -678,9 +678,7 @@ internal class ChildFactory @Inject constructor(
deeplink = mode.deeplink,
)
is AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings -> FromBannerInSettings
is AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain -> FromBannerOnMain(
userWalletId = mode.userWalletId,
)
is AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain -> FromBannerOnMain
},
componentFactory = tangemPayOnboardingComponentFactory,
)

View file

@ -444,9 +444,7 @@ sealed class AppRoute(val path: String) : Route {
) : Mode()
@Serializable
data class FromBannerOnMain(
val userWalletId: UserWalletId,
) : Mode()
data object FromBannerOnMain : Mode()
@Serializable
data object FromBannerInSettings : Mode()

View file

@ -48,6 +48,7 @@ class AccountPortfolioItemUMConverter(
)
UserWalletItemUM.Information.Loaded(text)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
private fun getImageState(account: Account.CryptoPortfolio) = when (account) {
@ -55,6 +56,7 @@ class AccountPortfolioItemUMConverter(
name = account.accountName.toUM().value,
icon = CryptoPortfolioIconConverter.convert(account.icon),
)
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
private fun getBalanceInfo(): UserWalletItemUM.Balance {

View file

@ -2,7 +2,10 @@ package com.tangem.common.ui.news
import android.content.res.Configuration
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CardColors
import androidx.compose.material3.Text
@ -11,6 +14,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -21,15 +26,12 @@ import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
@ -65,52 +67,61 @@ private fun TrendingArticle(
colors: CardColors,
modifier: Modifier = Modifier,
) {
Column(
Box(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(colors.containerColor)
.clickable { onArticleClick() }
.padding(vertical = 24.dp, horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
.clickable { onArticleClick() },
) {
Text(
modifier = Modifier
.background(
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
shape = RoundedCornerShape(8.dp),
)
.padding(horizontal = 8.dp, vertical = 4.dp),
text = stringResourceSafe(R.string.feed_trending_now),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.icon.accent,
Image(
modifier = Modifier.matchParentSize(),
painter = painterResource(R.drawable.bg_trending_article),
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
Column(
modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier
.background(
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
shape = RoundedCornerShape(8.dp),
)
.padding(horizontal = 8.dp, vertical = 4.dp),
text = stringResourceSafe(R.string.feed_trending_now),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.icon.accent,
)
SpacerH(12.dp)
SpacerH(12.dp)
Text(
text = articleConfigUM.title,
color = if (articleConfigUM.isViewed) {
TangemTheme.colors.text.tertiary
} else {
TangemTheme.colors.text.primary1
},
style = TangemTheme.typography.h3,
textAlign = TextAlign.Center,
)
Text(
text = articleConfigUM.title,
color = if (articleConfigUM.isViewed) {
TangemTheme.colors.text.tertiary
} else {
TangemTheme.colors.text.primary1
},
style = TangemTheme.typography.h3,
textAlign = TextAlign.Center,
)
SpacerH(8.dp)
SpacerH(8.dp)
ArticleInfo(
score = articleConfigUM.score,
createdAt = articleConfigUM.createdAt.resolveReference(),
)
ArticleInfo(
score = articleConfigUM.score,
createdAt = articleConfigUM.createdAt.resolveReference(),
)
SpacerH(32.dp)
SpacerH(32.dp)
Tags(
modifier = Modifier.padding(horizontal = 46.dp),
tags = articleConfigUM.tags.toImmutableList(),
)
Tags(
modifier = Modifier.padding(horizontal = 46.dp),
tags = articleConfigUM.tags.toImmutableList(),
)
}
}
}
@ -182,46 +193,14 @@ private fun DefaultArticle(
overflow = TextOverflow.Ellipsis,
)
SpacerHMax(modifier = Modifier.padding(bottom = 16.dp))
SpacerH(16.dp)
SpacerHMax()
Tags(tags = articleConfigUM.tags.toImmutableList())
}
}
@Composable
private fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
FlowRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
maxItemsInEachRow = MAX_TAGS_COUNT_TO_SHOW + 1,
maxLines = 1,
itemVerticalAlignment = Alignment.Bottom,
) {
val displayedTags = if (tags.size > MAX_TAGS_COUNT_TO_SHOW) {
tags.take(MAX_TAGS_COUNT_TO_SHOW)
} else {
tags
}
displayedTags.forEach { tag ->
Label(state = tag)
}
if (tags.size > MAX_TAGS_COUNT_TO_SHOW) {
val remainingItems = tags.size - MAX_TAGS_COUNT_TO_SHOW
Label(
state = LabelUM(
text = TextReference.Str("${StringsSigns.PLUS}$remainingItems"),
maxLines = 1,
),
)
}
}
}
private const val MAX_TAGS_COUNT_TO_SHOW = 3
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable

View file

@ -0,0 +1,128 @@
package com.tangem.common.ui.news
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.layout.SubcomposeMeasureScope
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.ImmutableList
private val TAG_SPACING = 4.dp
/**
* A layout that displays a list of tags, automatically calculating how many can fit
* in a single line. If not all tags fit, it shows an overflow indicator (e.g., "+N").
*
* @param tags The immutable list of [LabelUM] to display.
* @param modifier The modifier to be applied to the layout.
*/
@Composable
internal fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
if (tags.isEmpty()) return
SubcomposeLayout(modifier = modifier) { constraints ->
val spacingPx = TAG_SPACING.roundToPx()
val tagPlaceables = subcompose(ContentSlot.Tags) {
tags.forEach { tag ->
Label(state = tag)
}
}.map { it.measure(constraints) }
val layoutInfo = calculateLayoutInfo(
maxWidth = constraints.maxWidth,
spacingPx = spacingPx,
tagPlaceables = tagPlaceables,
)
val overflowPlaceable = layoutInfo.overflowCount.takeIf { it > 0 }?.let {
subcompose(ContentSlot.Overflow) {
OverflowLabel(count = it)
}.first().measure(constraints)
}
val visiblePlaceables = tagPlaceables.take(layoutInfo.visibleCount)
val allVisibleItems = visiblePlaceables + listOfNotNull(overflowPlaceable)
val width = calculateRowWidth(
placeables = allVisibleItems,
spacingPx = spacingPx,
).coerceIn(constraints.minWidth, constraints.maxWidth)
val height = allVisibleItems.maxOfOrNull { it.height } ?: 0
layout(width = width, height = height) {
var xPosition = 0
allVisibleItems.forEachIndexed { index, placeable ->
if (index > 0) {
xPosition += spacingPx
}
val yPosition = height - placeable.height
placeable.placeRelative(xPosition, yPosition)
xPosition += placeable.width
}
}
}
}
/**
* Calculates how many tags can be displayed and how many are overflowing.
*/
private fun SubcomposeMeasureScope.calculateLayoutInfo(
maxWidth: Int,
spacingPx: Int,
tagPlaceables: List<Placeable>,
): TagsLayoutInfo {
val totalWidthWithoutOverflow = calculateRowWidth(tagPlaceables, spacingPx)
if (totalWidthWithoutOverflow <= maxWidth) {
return TagsLayoutInfo(visibleCount = tagPlaceables.size, overflowCount = 0)
}
for (count in tagPlaceables.indices.reversed()) {
val visibleTags = tagPlaceables.take(count)
val overflowCount = tagPlaceables.size - count
val tempOverflowPlaceable = subcompose("overflow_temp_$overflowCount") {
OverflowLabel(count = overflowCount)
}.first().measure(Constraints())
val widthWithOverflow = calculateRowWidth(
placeables = visibleTags + tempOverflowPlaceable,
spacingPx = spacingPx,
)
if (widthWithOverflow <= maxWidth) {
return TagsLayoutInfo(visibleCount = count, overflowCount = overflowCount)
}
}
return TagsLayoutInfo(visibleCount = 0, overflowCount = tagPlaceables.size)
}
@Composable
private fun OverflowLabel(count: Int) {
Label(
state = LabelUM(
text = TextReference.Str("${StringsSigns.PLUS}$count"),
maxLines = 1,
),
)
}
private fun calculateRowWidth(placeables: List<Placeable>, spacingPx: Int): Int {
if (placeables.isEmpty()) return 0
val tagsWidth = placeables.sumOf { it.width }
val spacingWidth = spacingPx * (placeables.size - 1).coerceAtLeast(0)
return tagsWidth + spacingWidth
}
private data class TagsLayoutInfo(val visibleCount: Int, val overflowCount: Int)
private enum class ContentSlot { Tags, Overflow }

View file

@ -89,6 +89,7 @@ sealed class AnalyticsParam {
data object ImportWallet : ScreensSources("Import Wallet")
data object CreateWalletIntro : ScreensSources("Create Wallet Intro")
data object AddNewWallet : ScreensSources("Add New Wallet")
data object AddNew : ScreensSources("Add New")
data object CreateWallet : ScreensSources("Create Wallet")
data object NewsList : ScreensSources("News List")
data object NewsLink : ScreensSources("News Link")

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.promotion.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class PromoBannerV2Response(
@Json(name = "promotions")
val promotions: List<PromoBannerResponse>,
)

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.promotion.models.PromoBannerResponse
import com.tangem.datasource.api.promotion.models.PromoBannerV2Response
import com.tangem.datasource.api.promotion.models.StoryContentResponse
import com.tangem.datasource.api.tangemTech.models.*
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
@ -188,6 +189,12 @@ interface TangemTechApi {
@Query("programName") name: String,
@Header("Cache-Control") cacheControl: String = "max-age=600",
): ApiResponse<PromoBannerResponse>
@GET("/v2/promotion")
suspend fun getPromoBannersV2(
@Query("walletId") walletId: String,
@Header("Cache-Control") cacheControl: String = "max-age=600",
): ApiResponse<PromoBannerV2Response>
// endregion
/**

View file

@ -899,6 +899,9 @@
<string name="notification_sepa_title">Krypto mit SEPA kaufen</string>
<string name="notification_visa_waitlist_promo_text">Trage dich in die Warteliste ein und erhalte eine Zahlungskarte, die es so noch nie gab.</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
<string name="notification_yield_promo_button">Bedingungen</string>
<string name="notification_yield_promo_text">Zahlen Sie mindestens $100 ein, halten Sie den Betrag 30 Tage und erhalten Sie $10.</string>
<string name="notification_yield_promo_title">Yield-Mode-Kampagne</string>
<string name="onboarding_access_code_feature_1_description">Du musst einen einzigen Zugangscode einrichten, um alle deine Geräte zu schützen</string>
<string name="onboarding_access_code_feature_1_title">Schützen</string>
<string name="onboarding_access_code_feature_2_description">Du kannst später auf jeder Karte oder Ring einen individuellen Zugangscode einrichten</string>

View file

@ -299,6 +299,7 @@
<string name="common_go_to_token">Ir al token</string>
<string name="common_got_it">Entendido</string>
<string name="common_hide">Ocultar</string>
<string name="common_hold_to">Mantener para %s</string>
<string name="common_hour">hora</string>
<string name="common_import">Importe</string>
<string name="common_in_progress">En progreso</string>
@ -323,6 +324,7 @@
<string name="common_no">No</string>
<string name="common_no_address">Ninguna dirección</string>
<string name="common_not_added">No agregadas</string>
<string name="common_not_available">No disponible</string>
<string name="common_not_now">Ahora no</string>
<string name="common_now">Ahora</string>
<string name="common_ok">OK</string>
@ -373,6 +375,7 @@
<string name="common_swap">Intercambiar</string>
<string name="common_tangem">Tangem</string>
<string name="common_tangem_wallet">Tangem Wallet</string>
<string name="common_tap_and_hold_hint">Toque y mantenga presionado</string>
<string name="common_terms_and_conditions">términos y condiciones</string>
<string name="common_terms_of_use">Condiciones de uso</string>
<string name="common_to">A</string>
@ -462,6 +465,11 @@
<string name="domain_receive_assets_onboarding_description">Enviar activos a otras redes resultará en una pérdida permanente.</string>
<string name="domain_receive_assets_onboarding_network_name">Red de %s</string>
<string name="domain_receive_assets_onboarding_title">Envíe fondos utilizando solo</string>
<string name="earn_best_opportunities">Las mejores oportunidades</string>
<string name="earn_filter_all_networks">Todas las redes</string>
<string name="earn_filter_all_types">Todos los tipos</string>
<string name="earn_mostly_used">Mayormente usado</string>
<string name="earn_title">Ganar</string>
<string name="email_preface_wc_error">Hola equipo de soporte, he encontrado un error con el código: %s</string>
<string name="email_subject_wc_error">Error de WalletConnect</string>
<string name="error_wrong_wallet_tapped">Ha usado una tarjeta de otra billetera. Toque la tarjeta asociada con esta billetera</string>
@ -561,6 +569,8 @@
<string name="feedback_subject_support_tangem">Comentarios en Tangem</string>
<string name="feedback_subject_tx_failed">No se puede completar una transacción</string>
<string name="feedback_token_description_error">Error de descripción de moneda</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Fondos insuficientes</string>
<string name="gasless_transaction_fee">Tarifa de transacción</string>
<string name="generic_error">Ocurrió un error</string>
<string name="generic_error_code">Ocurrió un error. Código: %s</string>
<string name="generic_requires_memo_error">Requiere memo</string>
@ -610,6 +620,7 @@
<string name="hw_backup_need_title">Finalizar la copia de seguridad primero</string>
<string name="hw_backup_no_backup">Incompleto</string>
<string name="hw_backup_section_other_title">Otros métodos</string>
<string name="hw_backup_seed_code_description">Guarde su frase de recuperación en un lugar seguro y manténgala privada para proteger sus fondos y configure un código de acceso para mayor seguridad.</string>
<string name="hw_backup_seed_description">Guarde su frase de recuperación en un lugar seguro y manténgala en privado para proteger sus fondos.</string>
<string name="hw_backup_seed_title">Frase de recuperación</string>
<string name="hw_backup_to_secure_description">Para proteger su billetera con un código de acceso, complete el proceso de copia de seguridad.</string>
@ -896,6 +907,9 @@
<string name="notification_sepa_title">Comprar criptomonedas con SEPA</string>
<string name="notification_visa_waitlist_promo_text">Únete a la lista de espera y consigue una tarjeta de pago como ninguna otra.</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
<string name="notification_yield_promo_button">Términos y condiciones</string>
<string name="notification_yield_promo_text">Deposite $100 o más, manténgalo durante 30 días y reciba $10.</string>
<string name="notification_yield_promo_title">¡Únase a la campaña Yield Mode!</string>
<string name="onboarding_access_code_feature_1_description">Configure un código de acceso único para proteger todos sus dispositivos.</string>
<string name="onboarding_access_code_feature_1_title">Proteger</string>
<string name="onboarding_access_code_feature_2_description">Puede configurar un código de acceso individual en cada tarjeta más adelante</string>
@ -995,6 +1009,7 @@
<string name="onboarding_wallet_info_title_fourth">Restaurar código de acceso</string>
<string name="onboarding_wallet_info_title_second">Tarjetas idénticas</string>
<string name="onboarding_wallet_info_title_third">Código de acceso</string>
<string name="only_one_mobile_wallet_explanation">Solo puede tener una billetera móvil. Actualícela a la billetera de hardware Tangem o añada una nueva billetera de hardware.</string>
<string name="onramp_all_offers_button_title">Todas las ofertas</string>
<string name="onramp_avaiable_with_payment_methods">Disponible con %s</string>
<string name="onramp_choose_provider_title_hint">La compra de criptomonedas en Tangem se lleva a cabo utilizando proveedores externos.</string>
@ -1266,6 +1281,7 @@
<string name="staking_amount_tron_integer_error">La cantidad de staking se redondeará a %1$s TRX debido a las reglas de la red.</string>
<string name="staking_amount_tron_integer_error_unstaking">La cantidad de unstaking se redondeará a %1$s TRX debido a las reglas de la red.</string>
<string name="staking_apr_earn_badge">APR %1$s%%</string>
<string name="staking_cardano_details_rewards_info_text">Sus recompensas de staking comenzarán después de 5 épocas (~25 días) mientras su delegación esté registrada y contabilizada por la red.</string>
<string name="staking_claim_unstaked">Reclamar lo que no está en staking</string>
<string name="staking_details_account_fee">Tarifa de staking de la cuenta </string>
<string name="staking_details_account_fee_info">Una cuenta de staking es una cuenta especial donde se almacenan los tokens SOL de staking. Se crea cuando delegas sus tokens a un validador para participar en la validación de transacciones y ganar recompensas. Se cobra una pequeña tarifa por crear la cuenta de staking, que se devuelve una vez que se completa el staking.</string>
@ -1276,6 +1292,7 @@
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_auto_claiming_rewards_daily_text">Las recompensas se acumulan automáticamente en su saldo de staking diariamente.</string>
<string name="staking_details_autocompound_rewards_earned">Las recompensas se acumulan en su saldo de staking. Fondos ganados: %s</string>
<string name="staking_details_available">Disponible</string>
<string name="staking_details_average_reward_rate">Tasa de recompensa promedio</string>
<string name="staking_details_banner_text">¿Cómo funciona el staking?</string>
@ -1420,6 +1437,7 @@
<string name="swap_story_third_subtitle">Sencillo e intuitivo, permite cambiar tokens con solo unos cuantos toques</string>
<string name="swap_story_third_title">Simplemente cómodo</string>
<string name="swap_via_provider">Intercambio a través del proveedor</string>
<string name="swap_your_assets_title">Sus activos</string>
<string name="swapping_alert_cex_description">La cantidad incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario.</string>
<string name="swapping_alert_cex_description_with_slippage">El importe incluye:\n- comisión del proveedor de servicios\n- comisión de red por el envío de %1$s desde el exchange de vuelta a la dirección del usuario. \n\nEl deslizamiento (slippage) del proveedor es de hasta %2$s</string>
<string name="swapping_alert_dex_description">El importe incluye los honorarios del proveedor de servicios.</string>
@ -1434,6 +1452,7 @@
<string name="swapping_insufficient_funds">Fondos insuficientes</string>
<string name="swapping_permission_header">Dar autorización</string>
<string name="swapping_swap_action">Intercambiar</string>
<string name="swapping_swap_action_in_progress">Intercambiando...</string>
<string name="swapping_to_title">Usted recibe</string>
<string name="swapping_token_list_title">Elige token</string>
<string name="swapping_token_not_available">no disponible</string>
@ -1452,6 +1471,9 @@
<string name="tangem_pay_freeze_card_freeze">Mantén tu dinero seguro. Puedes desbloquear en cualquier momento.</string>
<string name="tangem_pay_freeze_card_success">Tu tarjeta está congelada.</string>
<string name="tangem_pay_get_help">Obtener ayuda</string>
<string name="tangem_pay_history_item_spend_mc_declined_reason">Razón: %s</string>
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s · %s</string>
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
<string name="tangem_pay_other">Otro</string>
<string name="tangem_pay_rooted_device_subtitle">No se puede usar en dispositivos rooteados</string>
<string name="tangem_pay_status_completed">Completado</string>
@ -1528,10 +1550,15 @@
<string name="tangempay_kyc_confirm_cancellation_description">¿Seguro que desea detener el proceso KYC? Puede retomarlo en cualquier momento.</string>
<string name="tangempay_kyc_failed_description">No pudimos verificar tu perfil. Si tienes alguna pregunta, contacta con el soporte.</string>
<string name="tangempay_kyc_failed_title">Lamentablemente, no pudimos verificar tu identidad</string>
<string name="tangempay_kyc_has_failed">El proceso KYC ha fallado</string>
<string name="tangempay_kyc_in_progress">KYC en curso</string>
<string name="tangempay_kyc_in_progress_notification_button">Ver estado</string>
<string name="tangempay_kyc_in_progress_notification_title">KYC en progreso para Tangem Pay</string>
<string name="tangempay_kyc_in_progress_popup_description">Los documentos suelen verificarse automáticamente en menos de 5 minutos. En casos excepcionales que requieran revisión manual, el proceso puede tardar hasta 48 horas.</string>
<string name="tangempay_kyc_rejected">KYC rechazado</string>
<string name="tangempay_kyc_rejected_button_text">Ocultar el bloque KYC</string>
<string name="tangempay_kyc_rejected_description">Lo sentimos, no pudimos verificarle</string>
<string name="tangempay_kyc_rejected_description_span">su perfil.</string>
<string name="tangempay_onboarding_banner_description">Obtén tu tarjeta virtual Tangem Visa gratuita</string>
<string name="tangempay_onboarding_banner_title">Usa USDC para pagos cotidianos</string>
<string name="tangempay_onboarding_get_card_button_text">Obtener tarjeta</string>
@ -1818,7 +1845,17 @@
<string name="warning_button_ok">Entendido</string>
<string name="warning_button_really_cool">¡Realmente genial!</string>
<string name="warning_button_refresh">Actualizar</string>
<string name="warning_clore_migration_button">Iniciar migración</string>
<string name="warning_clore_migration_copy_button">Copiar</string>
<string name="warning_clore_migration_error_signing_not_supported">La firma de mensajes no es compatible con esta red</string>
<string name="warning_clore_migration_error_wallet_manager_not_found">No se puede firmar el mensaje. Por favor, inténtelo de nuevo.</string>
<string name="warning_clore_migration_message">Según la documentación oficial de Clore, todas las monedas recibidas antes del 21 de diciembre serán migradas a Clore (token ERC-20); las monedas recibidas después de esa fecha no lo serán. Se está desarrollando una solución de transferencia — mantente atento.</string>
<string name="warning_clore_migration_message_label">Mensaje</string>
<string name="warning_clore_migration_open_portal_button">Abrir el portal de reclamaciones</string>
<string name="warning_clore_migration_sheet_description">Para mantener el acceso a sus fondos, comience la migración de acuerdo con las pautas oficiales de Clore.</string>
<string name="warning_clore_migration_sheet_title">Migración de la red Clore</string>
<string name="warning_clore_migration_sign_button">Firmar</string>
<string name="warning_clore_migration_signature_label">Firma</string>
<string name="warning_clore_migration_title">Migración de la red Clore</string>
<string name="warning_demo_mode_message">Actualmente estás en el modo Demo</string>
<string name="warning_demo_mode_title">Modo demo activo</string>

View file

@ -43,6 +43,7 @@
<string name="account_form_create_button">Ajouter un compte</string>
<string name="account_form_edit_button">Enregistrer</string>
<string name="account_form_name">Nom du compte</string>
<string name="account_form_name_already_exist_error_description">Un compte portant ce nom existe déjà. Veuillez choisir un autre nom.</string>
<string name="account_form_name_already_exist_error_title">Nom de compte déjà utilisé</string>
<string name="account_form_placeholder_edit_account">Compte</string>
<string name="account_form_placeholder_new_account">Nouveau compte</string>
@ -298,6 +299,7 @@
<string name="common_go_to_token">Aller au jeton</string>
<string name="common_got_it">Compris</string>
<string name="common_hide">Cacher</string>
<string name="common_hold_to">Maintenir contre %s</string>
<string name="common_hour">heure</string>
<string name="common_import">Importez</string>
<string name="common_in_progress">En cours</string>
@ -322,6 +324,7 @@
<string name="common_no">Non</string>
<string name="common_no_address">Aucune adresse</string>
<string name="common_not_added">Non ajouté</string>
<string name="common_not_available">Indisponible</string>
<string name="common_not_now">Pas maintenant</string>
<string name="common_now">Maintenant</string>
<string name="common_ok">OK</string>
@ -372,6 +375,7 @@
<string name="common_swap">Échanger</string>
<string name="common_tangem">Tangem</string>
<string name="common_tangem_wallet">Tangem Wallet</string>
<string name="common_tap_and_hold_hint">Appuyez et maintenez enfoncé</string>
<string name="common_terms_and_conditions">termes et conditions</string>
<string name="common_terms_of_use">Conditions d\'utilisation</string>
<string name="common_to">À</string>
@ -399,6 +403,8 @@
<string name="common_yield_mode">Mode Rendement</string>
<string name="contract_address_copied_message">Adresse du contrat copiée !</string>
<string name="currency_subtitle_expanded">Réseaux disponibles</string>
<string name="custom_token_another_account_dialog_description">La dérivation de votre token correspond à la dérivation de %1$s. Votre token sera ajouté à ce compte.</string>
<string name="custom_token_another_account_dialog_title">La dérivation appartient à un autre compte.</string>
<string name="custom_token_another_account_snackbar_text">Jeton ajouté au compte %1$s</string>
<string name="custom_token_contract_address_input_title">Adresse du contrat</string>
<string name="custom_token_creation_error_invalid_contract_address">L\'adresse du contrat n\'est pas valide</string>
@ -459,6 +465,11 @@
<string name="domain_receive_assets_onboarding_description">Lenvoi dactifs sur dautres réseaux entraînera une perte définitive.</string>
<string name="domain_receive_assets_onboarding_network_name">%s réseau</string>
<string name="domain_receive_assets_onboarding_title">Envoyez des fonds en utilisant uniquement</string>
<string name="earn_best_opportunities">Meilleures opportunités</string>
<string name="earn_filter_all_networks">Tous les réseaux</string>
<string name="earn_filter_all_types">Tous les types</string>
<string name="earn_mostly_used">Principalement utilisé</string>
<string name="earn_title">Gagner</string>
<string name="email_preface_wc_error">Bonjour équipe de support, jai rencontré une erreur avec le code : %s</string>
<string name="email_subject_wc_error">Erreur WalletConnect</string>
<string name="error_wrong_wallet_tapped">Vous avez utilisé une carte d\'un autre portefeuille. Appuyez sur la carte associée à ce portefeuille</string>
@ -558,6 +569,8 @@
<string name="feedback_subject_support_tangem">Commentaires sur Tangem</string>
<string name="feedback_subject_tx_failed">Impossible d\'effectuer une transaction</string>
<string name="feedback_token_description_error">Erreur de description de la pièce</string>
<string name="gasless_not_enough_funds_to_cover_token_fee">Fonds insuffisants</string>
<string name="gasless_transaction_fee">Frais de transaction</string>
<string name="generic_error">Une erreur s\'est produite</string>
<string name="generic_error_code">Une erreur s\'est produite. Code:%s</string>
<string name="generic_requires_memo_error">Un mémo est requis</string>
@ -607,6 +620,7 @@
<string name="hw_backup_need_title">Finalisez d\'abord la sauvegarde.</string>
<string name="hw_backup_no_backup">Incomplet</string>
<string name="hw_backup_section_other_title">Autres méthodes</string>
<string name="hw_backup_seed_code_description">Conservez votre seed phrase dans un endroit sûr et gardez-la confidentielle afin de protéger vos fonds, puis configurez un code d\'accès pour plus de sécurité.</string>
<string name="hw_backup_seed_description">Conservez votre seed phrase dans un endroit sûr et gardez-la confidentielle afin de protéger vos fonds.</string>
<string name="hw_backup_seed_title">Seed phrase</string>
<string name="hw_backup_to_secure_description">Pour sécuriser votre wallet avec un code d\'accès, effectuez la procédure de sauvegarde.</string>
@ -644,7 +658,7 @@
<string name="hw_upgrade_error_wallet2_card_required">Une erreur s\'est produite pendant l\'opération.</string>
<string name="hw_upgrade_funds_access_description">Vos fonds restent en sécurité et entièrement accessibles pendant toute la durée du processus.</string>
<string name="hw_upgrade_funds_access_title">Accès aux fonds</string>
<string name="hw_upgrade_general_security_description">Après la mise à niveau, votre portefeuille mobile sera supprimé de l\'application et stocké sur votre hardware wallet; votre seedphrase restera en votre possession.</string>
<string name="hw_upgrade_general_security_description">Après la mise à niveau, votre portefeuille mobile sera supprimé de l\'application et stocké sur votre hardware wallet; votre seed phrase restera en votre possession.</string>
<string name="hw_upgrade_general_security_title">Sécurité générale</string>
<string name="hw_upgrade_key_migration_description">Les clés privées seront transférées de l\'application vers votre hardware wallet Tangem.</string>
<string name="hw_upgrade_key_migration_title">Migration des clés</string>
@ -893,6 +907,9 @@
<string name="notification_sepa_title">Acheter des cryptomonnaies avec SEPA</string>
<string name="notification_visa_waitlist_promo_text">Rejoignez la liste dattente et obtenez une carte de paiement pas comme les autres.</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
<string name="notification_yield_promo_button">Conditions générales</string>
<string name="notification_yield_promo_text">Déposez 100 $ ou plus, conservez le dépôt pendant 30 jours et recevez 10 $.</string>
<string name="notification_yield_promo_title">Rejoignez la campagne Yield Mode !</string>
<string name="onboarding_access_code_feature_1_description">Vous devez définir un seul code d\'accès pour protéger tous vos appareils.</string>
<string name="onboarding_access_code_feature_1_title">Protéger</string>
<string name="onboarding_access_code_feature_2_description">Vous pourrez définir un code d\'accès individuel sur chaque carte plus tard</string>
@ -992,6 +1009,7 @@
<string name="onboarding_wallet_info_title_fourth">Restauration du code d\'accès</string>
<string name="onboarding_wallet_info_title_second">Cartes identiques</string>
<string name="onboarding_wallet_info_title_third">Code d\'accès</string>
<string name="only_one_mobile_wallet_explanation">Vous ne pouvez avoir qu\'un seul portefeuille mobile. Passez au portefeuille matériel Tangem ou ajoutez un nouveau portefeuille matériel.</string>
<string name="onramp_all_offers_button_title">Toutes les offres</string>
<string name="onramp_avaiable_with_payment_methods">Disponible avec %s</string>
<string name="onramp_choose_provider_title_hint">L\'achat de crypto-monnaies sur Tangem est assuré par des fournisseurs tiers selon leurs conditions.</string>
@ -1036,6 +1054,7 @@
<string name="onramp_title_available_from">Disponible auprès de</string>
<string name="onramp_title_available_up_to">Disponible jusqu\'à</string>
<string name="onramp_title_you_get">Vous obtenez</string>
<string name="onramp_tos_external_providers">Le service est fourni par un prestataire externe. Tangem n\'est pas responsable.</string>
<string name="onramp_transaction_status_footer_text">Vous pouvez vérifier l\'état de la transaction à partir de la page du jeton</string>
<string name="onramp_up_to_rate">Jusqu\'à</string>
<string name="onramp_via">Via</string>
@ -1248,18 +1267,21 @@
<string name="settings_forget_wallet_footer">Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau.</string>
<string name="settings_upgrade_banner_advantage_feature_title">Facile à utiliser</string>
<string name="settings_upgrade_banner_description">Gardez vos cryptomonnaies en sécurité et hors ligne. Aussi fin qu\'une carte de crédit, plus sûr qu\'un coffre-fort bancaire.</string>
<string name="settings_upgrade_banner_seed_phrase_feature_title">Pas de seedphrase</string>
<string name="settings_upgrade_banner_seed_phrase_feature_title">Pas de seed phrase</string>
<string name="settings_upgrade_banner_superiority_feature_title">Le meilleur hardware wallet</string>
<string name="settings_upgrade_banner_title">Tangem Hardware Wallet</string>
<string name="settings_wallet_name_title">Nom</string>
<string name="stake_token_description">Faites travailler votre jeton pour vous</string>
<string name="staking_account_initialization_footer">Les frais de réseau sont une petite somme à payer pour traiter et confirmer ta transaction sur la blockchain.</string>
<string name="staking_account_initialization_message">Pour commencer à staker, votre compte TON doit être activé avec une transaction de 1 TON. Les fonds restent sur votre compte, car cette étape sert uniquement à l\'activer pour le staking.</string>
<string name="staking_account_initialization_title">Activation du compte</string>
<string name="staking_alert_network_fee_updated_message">Les frais de réseau ont changé. Veuillez vérifier le nouveau montant avant de continuer.</string>
<string name="staking_alert_network_fee_updated_title">Frais de réseau mis à jour</string>
<string name="staking_amount_requirement_error">Le montant à staker doit être au moins %s</string>
<string name="staking_amount_tron_integer_error">Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau.</string>
<string name="staking_amount_tron_integer_error_unstaking">Le montant d\'annulation du staking sera arrondi à %1$s TRX en raison des règles du réseau.</string>
<string name="staking_apr_earn_badge">TAEG %1$s%%</string>
<string name="staking_cardano_details_rewards_info_text">Vos récompenses de staking commenceront après 5 époques (~25 jours), une fois que votre délégation aura été enregistrée et comptabilisée par le réseau.</string>
<string name="staking_claim_unstaked">Réclamation déstakée</string>
<string name="staking_details_account_fee">Frais de staking du compte</string>
<string name="staking_details_account_fee_info">Un compte de staking est un compte spécial où sont stockés les jetons SOL stakés. Il est créé lorsque vous déléguez vos jetons à un validateur pour participer à la validation des transactions et gagner des récompenses. Des frais minimes sont facturés pour la création du compte de staking, qui sont restitués une fois le staking terminé.</string>
@ -1270,6 +1292,7 @@
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_auto_claiming_rewards_daily_text">Les récompenses s\'accumulent automatiquement sur votre solde de staking quotidiennement.</string>
<string name="staking_details_autocompound_rewards_earned">Les récompenses sont ajoutées à votre solde de staking. Fonds gagnés : %s</string>
<string name="staking_details_available">Disponible</string>
<string name="staking_details_average_reward_rate">Taux de récompense moyen</string>
<string name="staking_details_banner_text">Qu\'est-ce que le Staking ?</string>
@ -1325,6 +1348,7 @@
<string name="staking_notification_restake_text">L\'option de restaker vous permet de déplacer vos fonds d\'un validateur à un autre sans avoir besoin de les déstaker.</string>
<string name="staking_notification_stake_entire_balance_text">Vous êtes sur le point de staker l\'intégralité de votre solde. Nous vous recommandons de laisser un petit montant pour couvrir les frais de réseau pour unstaking ou la réclamation des récompenses.</string>
<string name="staking_notification_ton_account_initialization_message">Pour commencer le staking, vous devez dabord activer votre compte TON</string>
<string name="staking_notification_ton_account_initialization_title">Activation du compte</string>
<string name="staking_notification_ton_activate_account">Pour commencer à staker TON, effectuez d\'abord une transaction sortante de n\'importe quel montant — cela activera votre portefeuille.</string>
<string name="staking_notification_ton_extra_reserve_info">Jusqu\'à 0,2 TON peuvent être requis en plus des frais de réseau pour terminer la transaction. Tout montant non utilisé sera remboursé.</string>
<string name="staking_notification_ton_extra_reserve_is_required">0,2 TON requis pour effectuer cette opération, en plus des frais de réseau. Veuillez recharger votre solde.</string>
@ -1413,6 +1437,7 @@
<string name="swap_story_third_subtitle">Simple et intuitif, vous permettant d\'échanger des jetons en quelques clics</string>
<string name="swap_story_third_title">Plus Simple Que Jamais</string>
<string name="swap_via_provider">Échange via le fournisseur</string>
<string name="swap_your_assets_title">Vos actifs</string>
<string name="swapping_alert_cex_description">Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %s depuis l\'échange vers l\'adresse de l\'utilisateur.</string>
<string name="swapping_alert_cex_description_with_slippage">Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %1$s depuis l\'échange vers l\'adresse de l\'utilisateur. \n\nLe slippage du fournisseur peut atteindre %2$s</string>
<string name="swapping_alert_dex_description">Le montant comprend les frais du fournisseur de services.</string>
@ -1427,6 +1452,7 @@
<string name="swapping_insufficient_funds">Fonds insuffisants</string>
<string name="swapping_permission_header">Donner l\'autorisation</string>
<string name="swapping_swap_action">Échanger</string>
<string name="swapping_swap_action_in_progress">Échange...</string>
<string name="swapping_to_title">Vous recevez</string>
<string name="swapping_token_list_title">Choisir le jeton</string>
<string name="swapping_token_not_available">non disponible</string>
@ -1445,6 +1471,9 @@
<string name="tangem_pay_freeze_card_freeze">Geler</string>
<string name="tangem_pay_freeze_card_success">Votre carte est gelée.</string>
<string name="tangem_pay_get_help">Obtenir de l\'aide</string>
<string name="tangem_pay_history_item_spend_mc_declined_reason">Raison: %s</string>
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s · %s</string>
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
<string name="tangem_pay_other">Autre</string>
<string name="tangem_pay_rooted_device_subtitle">Impossible à utiliser sur les appareils rootés</string>
<string name="tangem_pay_status_completed">Terminé</string>
@ -1521,10 +1550,15 @@
<string name="tangempay_kyc_confirm_cancellation_description">Êtes-vous sûr de vouloir interrompre le processus KYC ? Vous pouvez y revenir à tout moment.</string>
<string name="tangempay_kyc_failed_description">Nous n\'avons pas pu vérifier votre profil. Pour toute question, veuillez contacter le support.</string>
<string name="tangempay_kyc_failed_title">Malheureusement, nous n\'avons pas pu vérifier votre identité</string>
<string name="tangempay_kyc_has_failed">KYC a échoué</string>
<string name="tangempay_kyc_in_progress">KYC en cours</string>
<string name="tangempay_kyc_in_progress_notification_button">Voir le statut</string>
<string name="tangempay_kyc_in_progress_notification_title">KYC en cours pour Tangem Pay</string>
<string name="tangempay_kyc_in_progress_popup_description">Les documents sont généralement vérifiés automatiquement en moins de 5 minutes. Dans de rares cas nécessitant une vérification manuelle, le processus peut prendre jusquà 48 heures.</string>
<string name="tangempay_kyc_rejected">KYC rejeté</string>
<string name="tangempay_kyc_rejected_button_text">Masquer le bloc KYC</string>
<string name="tangempay_kyc_rejected_description">Désolé, nous n\'avons pas pu vérifier.</string>
<string name="tangempay_kyc_rejected_description_span">votre profil.</string>
<string name="tangempay_onboarding_banner_description">Obtenez votre carte virtuelle Tangem Visa gratuite</string>
<string name="tangempay_onboarding_banner_title">Utilisez USDC pour les paiements quotidiens</string>
<string name="tangempay_onboarding_get_card_button_text">Obtenir la carte</string>
@ -1579,6 +1613,7 @@
<string name="token_details_token_type_subtitle_no_standard">Jeton dans le %%image%% %1$s réseau</string>
<string name="token_details_unable_hide_alert_message">Le jeton %1$s (%2$s) est la principale devise du réseau %3$s et ne peut pas être masqué tant que vous avez d\'autres jetons de ce réseau dans la liste</string>
<string name="token_details_unable_hide_alert_title">Impossible de masquer %s</string>
<string name="token_receive_show_qr_code_title">Afficher le code QR</string>
<string name="token_swap_changelly_promotion_message">Échangez ce jeton contre un autre moyennant des frais de service de %1$s du %2$s au %3$s février.</string>
<string name="token_swap_changelly_promotion_title">Échangez avec Changelly, %s frais</string>
<string name="token_swap_promotion_button">Échangez maintenant</string>
@ -1710,6 +1745,12 @@
<string name="visa_unlock_notification_subtitle">Scannez votre carte pour déverrouiller l\'accès</string>
<string name="visa_unlock_notification_title">Déverrouillage nécessaire</string>
<string name="wallet_add_common_title">Choisissez comment ajouter votre portefeuille</string>
<string name="wallet_add_hardware_description">Scannez votre carte ou votre bague Tangem pour la restaurer ou l\'importer depuis un autre portefeuille.</string>
<string name="wallet_add_hardware_info_create">Créer un Hardware Wallet</string>
<string name="wallet_add_hardware_purchase">Vous souhaitez acheter un portefeuille Tangem ?</string>
<string name="wallet_add_import_seed_phrase">Importer la seed phrase</string>
<string name="wallet_add_mobile_description">Restaurez votre portefeuille sur votre téléphone ou importez-le depuis une autre application — pratique, mais moins sécurisé qu\'une carte Tangem.</string>
<string name="wallet_add_support_title">Que choisir ?</string>
<string name="wallet_balance_blockchain_unreachable_try_later">La blockchain n\'est pas accessible. Réessayez plus tard</string>
<string name="wallet_balance_missing_derivation">Scanner la carte ou la bague</string>
<string name="wallet_been_activated_message">Ce portefeuille a déjà été activé auparavant.\nSi cela n\'a pas été fait par vous, veuillez contacter le support.\nTangem ne vend jamais de portefeuilles avec le code d\'accès pré-généré.</string>
@ -1766,9 +1807,9 @@
<string name="wallet_import_navtitle">Ajouter un portefeuille existant</string>
<string name="wallet_import_scan_description">Dispositifs physiques qui stockent votre clé privée hors ligne en toute sécurité.</string>
<string name="wallet_import_scan_title">Scanner un Tangem Wallet</string>
<string name="wallet_import_seed_description">Importez un portefeuille existant à l\'aide de votre seedphrase.</string>
<string name="wallet_import_seed_description">Importez un portefeuille existant à l\'aide de votre seed phrase.</string>
<string name="wallet_import_seed_navtitle">Importer un wallet</string>
<string name="wallet_import_seed_title">Entrez la seedphrase</string>
<string name="wallet_import_seed_title">Entrez la seed phrase</string>
<string name="wallet_import_success_description">Vous avez importé votre portefeuille avec succès.</string>
<string name="wallet_import_success_navtitle">Importer un wallet</string>
<string name="wallet_import_success_title">Importation terminée</string>
@ -1984,7 +2025,7 @@
<string name="welcome_create_wallet_feature_delivery">Livraison rapide</string>
<string name="welcome_create_wallet_feature_one_tap">Commencez en un seul clic</string>
<string name="welcome_create_wallet_feature_seamless">Sans faille et sécurisé</string>
<string name="welcome_create_wallet_feature_seedphrase">Pas de seedphrase</string>
<string name="welcome_create_wallet_feature_seedphrase">Pas de seed phrase</string>
<string name="welcome_create_wallet_feature_use">Facile à utiliser</string>
<string name="welcome_create_wallet_hardware_description">Créez un harware wallet avec Tangem. Aussi fin qu\'une carte bancaire, aussi sûr qu\'un coffre-fort.</string>
<string name="welcome_create_wallet_mobile_description">Créer ou importer un portefeuille logiciel</string>
@ -2105,4 +2146,5 @@
<string name="yield_module_unable_to_cover_fee_title">Impossible de couvrir les frais %s</string>
<string name="yield_module_unavailable_subtitle">Le mode rendement n\'est pas disponible pour le moment. Veuillez réessayer plus tard.</string>
<string name="yield_module_unavailable_title">Le mode Rendement est indisponible</string>
<string name="yield_supply_chart_loading_error">Impossible de charger le graphique...</string>
</resources>

View file

@ -294,6 +294,7 @@
<string name="common_go_to_token">トークンへ移動</string>
<string name="common_got_it">わかりました</string>
<string name="common_hide">非表示</string>
<string name="common_hold_to">%sまで長押し</string>
<string name="common_hour">時間</string>
<string name="common_import">インポート</string>
<string name="common_in_progress">進行中</string>
@ -368,6 +369,7 @@
<string name="common_swap">スワップ</string>
<string name="common_tangem">Tangem</string>
<string name="common_tangem_wallet">Tangem Wallet</string>
<string name="common_tap_and_hold_hint">タップして長押し</string>
<string name="common_terms_and_conditions">利用規約</string>
<string name="common_terms_of_use">利用規約</string>
<string name="common_to">宛先</string>
@ -456,6 +458,11 @@
<string name="domain_receive_assets_onboarding_description">他のネットワークで資産を送金すると、永久に失われます。</string>
<string name="domain_receive_assets_onboarding_network_name">%sネットワーク</string>
<string name="domain_receive_assets_onboarding_title">下記のみを使用して資金を送金する</string>
<string name="earn_best_opportunities">おすすめ</string>
<string name="earn_filter_all_networks">すべてのネットワーク</string>
<string name="earn_filter_all_types">すべての種類</string>
<string name="earn_mostly_used">よく使われています</string>
<string name="earn_title">運用</string>
<string name="email_preface_wc_error">こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。</string>
<string name="email_subject_wc_error">WalletConnectエラー</string>
<string name="error_wrong_wallet_tapped">別のウォレットのカードまたはリングを使用しました。このウォレットにリンクしているカードまたはリングをタップしてください。</string>
@ -887,6 +894,9 @@
<string name="notification_sepa_title">SEPAで暗号資産を購入</string>
<string name="notification_visa_waitlist_promo_text">事前申し込み受付中。他にない特別なカードを、いち早く体験しよう。</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visaカード</string>
<string name="notification_yield_promo_button">利用規約</string>
<string name="notification_yield_promo_text">100ドル以上を入金し、30日間保持すると、10ドルを受け取れます。</string>
<string name="notification_yield_promo_title">Yield Mode キャンペーンに参加しよう!</string>
<string name="onboarding_access_code_feature_1_description">すべてのデバイスを保護するには、単一のアクセスコードを設定してください。</string>
<string name="onboarding_access_code_feature_1_title">保護する</string>
<string name="onboarding_access_code_feature_2_description">後で各カードおよびリングに個別のアクセスコードを設定できます。</string>
@ -935,11 +945,11 @@
<string name="onboarding_navbar_upgrade_wallet_biometrics">生体認証</string>
<string name="onboarding_seed_button_read_more">シードフレーズについてもっと読む</string>
<plurals name="onboarding_seed_generate_message_words_count">
<item quantity="other">これらの %d 単語を以下の順番通りに書き留め、安全かつ秘密の場所に保管してください。</item>
<item quantity="other">以下に表示される%d個の単語を順番どおりに書き留め、安全で他人に知られない場所に保管してください。</item>
</plurals>
<string name="onboarding_seed_generate_title">あなたのシードフレーズ</string>
<plurals name="onboarding_seed_generate_words_count">
<item quantity="other">%d 単語</item>
<item quantity="other">%d個の単語</item>
</plurals>
<string name="onboarding_seed_import_message">ウォレットをインポートするには、下のフィールドにシードフレーズを入力してください。</string>
<string name="onboarding_seed_intro_button_generate">シードフレーズを生成する</string>
@ -1252,6 +1262,7 @@
<string name="staking_amount_tron_integer_error">ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。</string>
<string name="staking_amount_tron_integer_error_unstaking">ネットワークルールにより、ステーキング解除の量は%1$s TRX に切り上げられます。</string>
<string name="staking_apr_earn_badge">APR %1$s%%</string>
<string name="staking_cardano_details_rewards_info_text">デリゲーションがネットワークに登録され、集計されるまでの間、ステーキング報酬は5エポック後約25日後から開始されます。</string>
<string name="staking_claim_unstaked">ステーキング解除分を請求する</string>
<string name="staking_details_account_fee">ステーキングアカウント手数料</string>
<string name="staking_details_account_fee_info">ステーキングアカウントとは、ステーキングされたSOLが保管される特別なアカウントです。トークンをバリデーターに委任し、取引の検証に参加して報酬を受け取る際に作成されます。ステーキングアカウントの作成には少額の手数料がかかりますが、ステーキング完了後に返金されます。</string>
@ -1441,6 +1452,9 @@
<string name="tangem_pay_freeze_card_freeze">一時停止</string>
<string name="tangem_pay_freeze_card_success">カードが凍結されています</string>
<string name="tangem_pay_get_help">サポートを受ける</string>
<string name="tangem_pay_history_item_spend_mc_declined_reason">理由:%s</string>
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s・%s</string>
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
<string name="tangem_pay_other">その他</string>
<string name="tangem_pay_rooted_device_subtitle">Root化された端末では使用できません</string>
<string name="tangem_pay_status_completed">完了</string>
@ -1643,25 +1657,69 @@
<string name="user_wallet_list_rename_popup_title">ウォレット名の変更</string>
<string name="user_wallet_list_unlock_all">すべてのロックを解除</string>
<string name="user_wallet_list_unlock_all_with">%sですべてをロック解除</string>
<string name="visa_balance_limits_details_aml_verified">AML認証済み</string>
<string name="visa_balance_limits_details_available">利用可能</string>
<string name="visa_balance_limits_details_blocked">ブロック中</string>
<string name="visa_balance_limits_details_debt">負債</string>
<string name="visa_balance_limits_details_limits">利用上限</string>
<string name="visa_balance_limits_details_no_otp_limit">その他OTPなし</string>
<string name="visa_balance_limits_details_single_transaction">単一の取引</string>
<string name="visa_balance_limits_details_total">合計</string>
<plurals name="visa_limits_available_for_days_title">
<item quantity="other">%d日間利用可能</item>
</plurals>
<string name="visa_main_available_balance_alert_message">利用可能残高は、保留中の取引、ブロック中の金額、デビット残高を考慮した、実際に利用できる資金を示します。</string>
<string name="visa_main_available_till_date">%1$sまで利用可能</string>
<string name="visa_main_balances_and_limits">残高と限度額</string>
<string name="visa_main_button_alert_missing_payment_account_info_message">支払いアカウントの情報が見つかりません。サポートにお問い合わせください。</string>
<string name="visa_main_button_alert_missing_token_info">トークンの情報を読み込めません。</string>
<string name="visa_main_button_alert_still_loading">情報を読み込み中です。しばらくお待ちください。</string>
<string name="visa_main_limits_alert_description_message">コストの管理、セキュリティの向上、リスク管理のために利用上限が設けられています。店舗でのカード決済では期間内に%1$s、サブスクリプションや支払いなどのその他の取引では%2$sまで利用できます。</string>
<string name="visa_onboarding_access_code_description">アクセスコードは、支払いアカウントの管理および不正アクセスからの保護に使用されます。</string>
<string name="visa_onboarding_access_code_navigation_title">アクセスコード</string>
<string name="visa_onboarding_account_activation_navigation_title">アカウント有効化</string>
<string name="visa_onboarding_approve_wallet_selector_notification_message">登録手続きを開始したウォレットを選択し、ブロックチェーン上でアカウントを作成するための取引に署名してください。</string>
<string name="visa_onboarding_cancel_activation">有効化をキャンセル</string>
<string name="visa_onboarding_close_alert_message">本当に終了してもよろしいですか?中断したところから後で続行できます。</string>
<string name="visa_onboarding_in_progress_description">長くはかかりません。アカウントを設定しています。</string>
<string name="visa_onboarding_in_progress_issuer_description">長くはかかりません。アクティベーションを完了しています。</string>
<string name="visa_onboarding_in_progress_title">準備完了です!</string>
<string name="visa_onboarding_other_wallet">その他のウォレット</string>
<string name="visa_onboarding_pin_code_description">4桁のコードを設定します。 \nお支払いの際に使用されます。</string>
<string name="visa_onboarding_pin_code_navigation_title">PINコード</string>
<string name="visa_onboarding_pin_code_title">PINコードを作成する</string>
<string name="visa_onboarding_pin_not_accepted">PINの認証に失敗しました。もう一度お試しいただくか、別のコードを使用してください。</string>
<string name="visa_onboarding_pin_validation_error_message">無効な暗証番号:連続や繰り返しを避けてください</string>
<string name="visa_onboarding_success_screen_description">準備完了です!</string>
<string name="visa_onboarding_tangem_approve_description">Tangemカードを用意し、タップして承認してください。</string>
<string name="visa_onboarding_tangem_approve_title">Tangemウォレットを準備してください。</string>
<string name="visa_onboarding_wallet_connect_description">\nサードパーティのWebサイト\nで接続手続きを完了し、その後Tangemアプリに戻ってください。</string>
<string name="visa_onboarding_wallet_connect_title">ウェブサイトに移動</string>
<string name="visa_onboarding_wallet_connection_navigation_title">ウォレット接続</string>
<string name="visa_onboarding_wallet_list_header">ウォレットを選択</string>
<string name="visa_onboarding_welcome_back_button_title">有効化を続行</string>
<string name="visa_onboarding_welcome_back_description">アカウントの設定を続けましょう。</string>
<string name="visa_onboarding_welcome_back_title">お帰りなさい!</string>
<string name="visa_onboarding_welcome_button_title">有効化を開始</string>
<string name="visa_onboarding_welcome_description">手順に従ってアカウントを設定してください。</string>
<string name="visa_onboarding_welcome_title">ようこそ!</string>
<string name="visa_transaction_details_blockchain_amount">ブロックチェーン上の金額</string>
<string name="visa_transaction_details_currency_code">通貨コード</string>
<string name="visa_transaction_details_date">日付</string>
<string name="visa_transaction_details_error_code">エラーコード</string>
<string name="visa_transaction_details_header">取引詳細</string>
<string name="visa_transaction_details_merchant_category_code">加盟店カテゴリーコード</string>
<string name="visa_transaction_details_merchant_city">加盟店の所在地</string>
<string name="visa_transaction_details_merchant_country_code">加盟店の国コード</string>
<string name="visa_transaction_details_merchant_name">加盟店名</string>
<string name="visa_transaction_details_request_id">リクエストID</string>
<string name="visa_transaction_details_status">ステータス</string>
<string name="visa_transaction_details_title">取引</string>
<string name="visa_transaction_details_transaction_amount">取引金額</string>
<string name="visa_transaction_details_transaction_hash">トランザクションハッシュ</string>
<string name="visa_transaction_details_transaction_request">取引リクエスト</string>
<string name="visa_transaction_details_transaction_status">取引ステータス</string>
<string name="visa_transaction_details_type">種類</string>
<string name="visa_tx_dispute_button">この取引に異議を唱える</string>
<string name="visa_unlock_notification_button">ロック解除</string>
<string name="visa_unlock_notification_subtitle">カードをスキャンしてアクセスロックを解除する</string>

View file

@ -925,6 +925,9 @@
<string name="notification_sepa_title">Покупайте крипту через SEPA</string>
<string name="notification_visa_waitlist_promo_text">Присоединяйтесь к списку ожидания и получите уникальную платёжную карту, не похожую ни на одну другую.</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
<string name="notification_yield_promo_button">Условия программы</string>
<string name="notification_yield_promo_text">Внесите депозит от $100, удерживайте его 30 дней и получите бонус $10</string>
<string name="notification_yield_promo_title">Подключите Режим доходности!</string>
<string name="onboarding_access_code_feature_1_description">Установите единый код доступа для защиты всех ваших карт или колец</string>
<string name="onboarding_access_code_feature_1_title">Защита</string>
<string name="onboarding_access_code_feature_2_description">Установите индивидуальный код доступа для каждой карты или кольца позже.</string>
@ -1800,7 +1803,17 @@
<string name="warning_button_ok">Понятно!</string>
<string name="warning_button_really_cool">Очень круто!</string>
<string name="warning_button_refresh">Обновить</string>
<string name="warning_clore_migration_button">Начать миграцию</string>
<string name="warning_clore_migration_copy_button">Копировать</string>
<string name="warning_clore_migration_error_signing_not_supported">Подписание сообщений не поддерживается в этой сети</string>
<string name="warning_clore_migration_error_wallet_manager_not_found">Невозможно подписать сообщение. Пожалуйста, попробуй позже.</string>
<string name="warning_clore_migration_message">Согласно официальной документации Clore, все монеты, полученные до 21 декабря, будут мигрированы в токен Clore (ERC-20); монеты, полученные после этой даты, — нет. Решение для перевода находится в разработке — следите за обновлениями.</string>
<string name="warning_clore_migration_message_label">Сообщение</string>
<string name="warning_clore_migration_open_portal_button">Открыть портал клейма</string>
<string name="warning_clore_migration_sheet_description">Чтобы сохранить доступ к своим средствам, начните миграцию в соответствии с официальной инструкцией Clore.</string>
<string name="warning_clore_migration_sheet_title">Миграция сети Clore</string>
<string name="warning_clore_migration_sign_button">Подписать</string>
<string name="warning_clore_migration_signature_label">Подпись</string>
<string name="warning_clore_migration_title">Миграция сети Clore</string>
<string name="warning_demo_mode_message">Вы находитесь в режиме демо</string>
<string name="warning_demo_mode_title">Демо режим включен</string>

View file

@ -44,6 +44,7 @@
<string name="account_form_edit_button">Зберегти</string>
<string name="account_form_name">Назва акаунту</string>
<string name="account_form_name_already_exist_error_description">Акаунт з такою назвою вже існує. Будь ласка, виберіть іншу назву.</string>
<string name="account_form_name_already_exist_error_title">Обліковий запис з таким ім’ям уже існує</string>
<string name="account_form_placeholder_edit_account">Акаунт</string>
<string name="account_form_placeholder_new_account">Новий акаунт</string>
<string name="account_form_title_create">Додати акаунт</string>
@ -908,6 +909,9 @@
<string name="notification_sepa_title">Купуйте криптовалюту через SEPA</string>
<string name="notification_visa_waitlist_promo_text">Приєднуйтесь до списку очікування та отримайте унікальну платіжну картку, якої ще не було.</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
<string name="notification_yield_promo_button">Умови та положення</string>
<string name="notification_yield_promo_text">Внесіть депозит від $100, утримуйте його 30 днів та отримайте $10.</string>
<string name="notification_yield_promo_title">Кампанія Yield Mode!</string>
<string name="onboarding_access_code_feature_1_description">Налаштуйте єдиний код доступу для захисту всіх ваших карток або кілець</string>
<string name="onboarding_access_code_feature_1_title">Захист</string>
<string name="onboarding_access_code_feature_2_description">Встановіть індивідуальний код доступу для кожної картки або кільця пізніше.</string>

View file

@ -86,6 +86,8 @@
<string name="address_qr_code_message_format">Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.</string>
<string name="address_type_default">Default</string>
<string name="address_type_legacy">Legacy</string>
<string name="alert_authentication_error_message">Try to reset biometrics on your device or contact support</string>
<string name="alert_authentication_error_title">Authentication error</string>
<string name="alert_button_how_to_scan">How to scan</string>
<string name="alert_button_request_support">Request support</string>
<string name="alert_button_try_again">Try again</string>
@ -908,6 +910,9 @@
<string name="notification_sepa_title">Buy Crypto with SEPA</string>
<string name="notification_visa_waitlist_promo_text">Join the waitlist and get a payment card unlike any other</string>
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
<string name="notification_yield_promo_button">Terms and conditions</string>
<string name="notification_yield_promo_text">Deposit $100+, hold for 30 days, get $10</string>
<string name="notification_yield_promo_title">Join the Yield Mode Campaign!</string>
<string name="onboarding_access_code_feature_1_description">Set up a single access code to protect all your devices.</string>
<string name="onboarding_access_code_feature_1_title">Protect</string>
<string name="onboarding_access_code_feature_2_description">Set an individual access code for each card or ring later.</string>
@ -1843,7 +1848,18 @@
<string name="warning_button_ok">Ok, Got it!</string>
<string name="warning_button_really_cool">Really cool!</string>
<string name="warning_button_refresh">Refresh</string>
<string name="warning_clore_migration_button">Start migration</string>
<string name="warning_clore_migration_copy_button">Copy</string>
<string name="warning_clore_migration_description">To keep access to your funds, start the migration according to the official Clore guidelines.</string>
<string name="warning_clore_migration_error_signing_not_supported">Message signing is not supported for this network</string>
<string name="warning_clore_migration_error_wallet_manager_not_found">Unable to sign message. Please try again.</string>
<string name="warning_clore_migration_message">According to Clores official documentation, all coins received before December 21 will be migrated to Clore (ERC-20 token); coins received after that date will not. A transfer solution is coming — stay tuned.</string>
<string name="warning_clore_migration_message_label">Message</string>
<string name="warning_clore_migration_open_portal_button">Open Claim portal</string>
<string name="warning_clore_migration_sheet_description">To continue using your Clore tokens, you must complete the token migration according to the information on the Claim Portal.</string>
<string name="warning_clore_migration_sheet_title">Clore Network Migration</string>
<string name="warning_clore_migration_sign_button">Sign</string>
<string name="warning_clore_migration_signature_label">Signature</string>
<string name="warning_clore_migration_title">Clore Network Migration</string>
<string name="warning_demo_mode_message">You are currently in the Demo mode</string>
<string name="warning_demo_mode_title">Demo mode active</string>

View file

@ -120,7 +120,7 @@ fun TokenItem(
composables.icon.invoke(
Modifier
.layoutId(layoutId = LayoutId.ICON)
.padding(end = TangemTheme.dimens.spacing8)
.padding(end = TangemTheme.dimens.spacing12)
.testTag(TokenElementsTestTags.TOKEN_ICON),
)
} else {
@ -128,7 +128,7 @@ fun TokenItem(
state = state.iconState,
modifier = Modifier
.layoutId(layoutId = LayoutId.ICON)
.padding(end = TangemTheme.dimens.spacing8)
.padding(end = TangemTheme.dimens.spacing12)
.testTag(TokenElementsTestTags.TOKEN_ICON),
)
}

View file

@ -7,8 +7,10 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalRippleConfiguration
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -32,6 +34,7 @@ import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.tokenlist.internal.GroupTitleItem
import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.ProvideSharedTransitionScope
@ -71,13 +74,13 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea
ProvideSharedTransitionScope(modifier) {
val iconSharedContentState = rememberSharedContentState(key = "icon_${state.id}")
val titleSharedContentState = rememberSharedContentState(key = "title_${state.id}")
val boundsTransform = BoundsTransform { _, _ -> tween(350) }
val boundsTransform = BoundsTransform { _, _ -> tween(250) }
AnimatedContent(
targetState = state.isExpanded,
transitionSpec = {
fadeIn(animationSpec = tween(350, delayMillis = 90))
.togetherWith(fadeOut(animationSpec = tween(350)))
fadeIn(animationSpec = tween(250, delayMillis = 90))
.togetherWith(fadeOut(animationSpec = tween(250)))
},
) { isExpanded ->
val animatedContentScope = this
@ -116,7 +119,7 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea
val textSize by animateFloatAsState(
targetValue = textStyle.fontSize.value,
animationSpec = tween(350),
animationSpec = tween(250),
)
TokenTitle(
@ -133,32 +136,34 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea
},
)
if (isExpanded) {
ExpandedPortfolioHeader(
state = state.tokenItemUM,
isCollapsable = state.isCollapsable,
composables = composables,
balanceTextModifier = Modifier
.animateEnterExit(
enter = fadeIn(tween(350)) +
slideInHorizontally(
tween(350),
initialOffsetX = { fullWidth -> fullWidth * 2 },
),
exit = fadeOut(
tween(350),
) + slideOutHorizontally(
tween(350),
targetOffsetX = { fullWidth -> fullWidth * 2 },
CompositionLocalProvider(LocalRippleConfiguration provides null) {
if (isExpanded) {
ExpandedPortfolioHeader(
state = state.tokenItemUM,
isCollapsable = state.isCollapsable,
isBalanceHidden = isBalanceHidden,
composables = composables,
balanceTextModifier = Modifier
.animateEnterExit(
enter = fadeIn(tween(250)) +
slideInHorizontally(
tween(250),
initialOffsetX = { fullWidth -> fullWidth },
),
exit = fadeOut(tween(250)) +
slideOutHorizontally(
tween(250),
targetOffsetX = { fullWidth -> fullWidth },
),
),
),
)
} else {
TokenItem(
state = state.tokenItemUM,
isBalanceHidden = isBalanceHidden,
composables = composables,
)
)
} else {
TokenItem(
state = state.tokenItemUM,
isBalanceHidden = isBalanceHidden,
composables = composables,
)
}
}
}
}
@ -180,6 +185,7 @@ fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: B
@Composable
fun ExpandedPortfolioHeader(
state: TokenItemState,
isBalanceHidden: Boolean,
isCollapsable: Boolean,
composables: TokenItemComposables?,
modifier: Modifier = Modifier,
@ -191,8 +197,10 @@ fun ExpandedPortfolioHeader(
.fillMaxWidth()
.clickable(onClick = { state.onItemClick?.invoke(state) })
.padding(
vertical = if (composables != null) 8.dp else 4.dp,
horizontal = 12.dp,
top = 12.dp,
bottom = 4.dp,
start = 12.dp,
end = 12.dp,
),
) {
if (composables != null) {
@ -227,12 +235,19 @@ fun ExpandedPortfolioHeader(
modifier = Modifier.weight(1f),
) {
if (composables != null) {
composables.title.invoke(Modifier.alignByBaseline())
composables.title.invoke(
Modifier
.padding(vertical = 2.dp)
.alignByBaseline(),
)
} else {
when (val titleState = state.titleState) {
TokenItemState.TitleState.Loading -> Unit
TokenItemState.TitleState.Locked -> Unit
is TokenItemState.TitleState.Content -> Text(
modifier = Modifier
.padding(vertical = 2.dp)
.alignByBaseline(),
text = titleState.text.resolveReference(),
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
@ -249,10 +264,10 @@ fun ExpandedPortfolioHeader(
}
Text(
text = text.orEmpty(),
text = text.orEmpty().orMaskWithStars(isBalanceHidden),
modifier = balanceTextModifier
.alignByBaseline()
.padding(horizontal = 8.dp),
.padding(horizontal = 4.dp),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption1,
overflow = TextOverflow.Ellipsis,

View file

@ -1,5 +1,9 @@
package com.tangem.core.ui.utils
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.BoundsTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.layout.Box
@ -7,6 +11,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
@Composable
fun TangemSharedTransitionLayout(
@ -31,6 +42,78 @@ fun ProvideSharedTransitionScope(modifier: Modifier = Modifier, content: @Compos
}
}
private val LocalSharedTransitionScope = staticCompositionLocalOf<SharedTransitionScope> {
error("No SharedTransitionScope provided")
private val LocalSharedTransitionScope = staticCompositionLocalOf<SharedTransitionScope> { StubSharedTransitionScope() }
private class StubSharedTransitionScope : SharedTransitionScope {
override val isTransitionActive: Boolean = false
override val Placeable.PlacementScope.lookaheadScopeCoordinates: LayoutCoordinates
get() = error("No LookaheadScopeCoordinates available in StubSharedTransitionScope")
override fun Modifier.skipToLookaheadSize(enabled: () -> Boolean): Modifier {
return this
}
override fun Modifier.renderInSharedTransitionScopeOverlay(
zIndexInOverlay: Float,
renderInOverlay: () -> Boolean,
): Modifier {
return this
}
override fun Modifier.sharedElement(
sharedContentState: SharedTransitionScope.SharedContentState,
animatedVisibilityScope: AnimatedVisibilityScope,
boundsTransform: BoundsTransform,
placeholderSize: SharedTransitionScope.PlaceholderSize,
renderInOverlayDuringTransition: Boolean,
zIndexInOverlay: Float,
clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip,
): Modifier {
return this
}
override fun Modifier.sharedBounds(
sharedContentState: SharedTransitionScope.SharedContentState,
animatedVisibilityScope: AnimatedVisibilityScope,
enter: EnterTransition,
exit: ExitTransition,
boundsTransform: BoundsTransform,
resizeMode: SharedTransitionScope.ResizeMode,
placeholderSize: SharedTransitionScope.PlaceholderSize,
renderInOverlayDuringTransition: Boolean,
zIndexInOverlay: Float,
clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip,
): Modifier {
return this
}
override fun Modifier.sharedElementWithCallerManagedVisibility(
sharedContentState: SharedTransitionScope.SharedContentState,
visible: Boolean,
boundsTransform: BoundsTransform,
placeholderSize: SharedTransitionScope.PlaceholderSize,
renderInOverlayDuringTransition: Boolean,
zIndexInOverlay: Float,
clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip,
): Modifier {
return this
}
override fun OverlayClip(clipShape: Shape): SharedTransitionScope.OverlayClip {
return object : SharedTransitionScope.OverlayClip {
override fun getClipPath(
sharedContentState: SharedTransitionScope.SharedContentState,
bounds: Rect,
layoutDirection: LayoutDirection,
density: Density,
): Path? {
return Path()
}
}
}
override fun LayoutCoordinates.toLookaheadCoordinates(): LayoutCoordinates {
return this
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -16,6 +16,7 @@ import com.tangem.data.account.store.ArchivedAccountsStoreFactory
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.di.NetworkMoshi
@ -117,11 +118,15 @@ internal object AccountDataModule {
accountsResponseStoreFactory: AccountsResponseStoreFactory,
userTokensSaver: UserTokensSaver,
accountTokenMigrationStore: AccountTokenMigrationStore,
eTagsStore: ETagsStore,
dispatchers: CoroutineDispatcherProvider,
): DefaultMainAccountTokensMigration {
return DefaultMainAccountTokensMigration(
accountsResponseStoreFactory = accountsResponseStoreFactory,
accountTokenMigrationStore = accountTokenMigrationStore,
userTokensSaver = userTokensSaver,
eTagsStore = eTagsStore,
dispatchers = dispatchers,
)
}
}

View file

@ -24,6 +24,8 @@ import com.tangem.datasource.api.tangemTech.models.orDefault
import com.tangem.datasource.utils.getSyncOrNull
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@ -77,6 +79,10 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
return migratedResponse
}
override fun get(userWalletId: UserWalletId): Flow<GetWalletAccountsResponse> {
return getAccountsResponseStore(userWalletId = userWalletId).data.filterNotNull()
}
override suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse? {
return getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull()
}

View file

@ -11,6 +11,7 @@ import com.tangem.data.account.converter.AccountNameConverter
import com.tangem.data.account.converter.toDerivationIndex
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.utils.assignTokens
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
@ -22,6 +23,10 @@ import com.tangem.domain.account.tokens.MainAccountTokensMigration
import com.tangem.domain.models.account.DerivationIndex
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import timber.log.Timber
/**
@ -37,8 +42,12 @@ internal class DefaultMainAccountTokensMigration(
private val accountsResponseStoreFactory: AccountsResponseStoreFactory,
private val accountTokenMigrationStore: AccountTokenMigrationStore,
private val userTokensSaver: UserTokensSaver,
private val eTagsStore: ETagsStore,
dispatchers: CoroutineDispatcherProvider,
) : MainAccountTokensMigration {
private val coroutineScope = CoroutineScope(dispatchers.default + SupervisorJob())
internal suspend fun migrate(userWalletId: UserWalletId): Either<Throwable, GetWalletAccountsResponse> = either {
val store = accountsResponseStoreFactory.create(userWalletId)
val response = store.getSyncOrNull()
@ -89,16 +98,7 @@ internal class DefaultMainAccountTokensMigration(
store.updateData { updatedResponse }
val userTokensResponse = updatedResponse.toUserTokensResponse()
userTokensSaver.pushWithRetryer(
userWalletId = userWalletId,
response = userTokensResponse,
onFailSend = {
val exception = IllegalStateException("Failed to push updated tokens after migration")
Timber.e(exception)
raise(exception)
},
)
pushTokens(userWalletId, updatedResponse)
return@either updatedResponse
}
@ -151,16 +151,7 @@ internal class DefaultMainAccountTokensMigration(
val selectedAccountName = AccountNameConverter.convertBack(selectedAccount.name)
accountTokenMigrationStore.store(userWalletId, mainAccountName to selectedAccountName)
val userTokensResponse = updatedResponse.toUserTokensResponse()
userTokensSaver.pushWithRetryer(
userWalletId = userWalletId,
response = userTokensResponse,
onFailSend = {
val exception = IllegalStateException("Failed to push updated tokens after migration")
Timber.e(exception)
raise(exception)
},
)
pushTokens(userWalletId, updatedResponse)
}
private fun Raise<Throwable>.findAccount(
@ -232,4 +223,20 @@ internal class DefaultMainAccountTokensMigration(
accountNodeValue == derivationIndex.value.toLong()
}
}
private suspend fun Raise<Throwable>.pushTokens(userWalletId: UserWalletId, response: GetWalletAccountsResponse) {
val userTokensResponse = response.toUserTokensResponse()
userTokensSaver.pushWithRetryer(
userWalletId = userWalletId,
response = userTokensResponse,
onFailSend = {
coroutineScope.launch {
eTagsStore.clear(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
}
val exception = IllegalStateException("Failed to push updated tokens after migration")
Timber.e(exception)
raise(exception)
},
)
}
}

View file

@ -20,6 +20,7 @@ import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResp
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.test.core.getEmittedValues
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.flow.MutableStateFlow
@ -340,6 +341,47 @@ class DefaultWalletAccountsFetcherTest {
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Get {
@Test
fun get() = runTest {
// Arrange
val response = createGetWalletAccountsResponse(userWalletId = userWalletId)
accountsResponseStoreFlow.value = response
// Act
val actual = getEmittedValues(fetcher.get(userWalletId))
// Assert
val expected = response
Truth.assertThat(actual).containsExactly(expected)
coVerifyOrder {
accountsResponseStoreFactory.create(userWalletId)
accountsResponseStore.data
}
}
@Test
fun `get if store is empty`() = runTest {
// Arrange
accountsResponseStoreFlow.value = null
// Act
val actual = getEmittedValues(fetcher.get(userWalletId))
// Assert
Truth.assertThat(actual).isEmpty()
coVerifyOrder {
accountsResponseStoreFactory.create(userWalletId)
accountsResponseStore.data
}
}
}
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Store {

View file

@ -6,6 +6,7 @@ import com.tangem.data.account.converter.createWalletAccountDTO
import com.tangem.data.account.store.AccountsResponseStore
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
@ -17,6 +18,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.test.core.assertEither
import com.tangem.test.core.assertEitherLeft
import com.tangem.test.core.assertEitherRight
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
@ -34,11 +36,14 @@ class DefaultMainAccountTokensMigrationTest {
private val accountsResponseStoreFlow = MutableStateFlow<GetWalletAccountsResponse?>(value = null)
private val accountTokenMigrationStore = mockk<AccountTokenMigrationStore>(relaxUnitFun = true)
private val userTokensSaver = mockk<UserTokensSaver>(relaxUnitFun = true)
private val eTagsStore = mockk<ETagsStore>(relaxUnitFun = true)
private val migration = DefaultMainAccountTokensMigration(
accountsResponseStoreFactory = accountsResponseStoreFactory,
accountTokenMigrationStore = accountTokenMigrationStore,
userTokensSaver = userTokensSaver,
eTagsStore = eTagsStore,
dispatchers = TestingCoroutineDispatcherProvider(),
)
private val userWalletId = UserWalletId("011")

View file

@ -2,6 +2,7 @@ package com.tangem.data.common.account
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
/**
* Component for fetching wallet accounts
@ -14,6 +15,9 @@ interface WalletAccountsFetcher {
@Throws
suspend fun fetch(userWalletId: UserWalletId): GetWalletAccountsResponse
/** Get wallet accounts by [userWalletId] */
fun get(userWalletId: UserWalletId): Flow<GetWalletAccountsResponse>
/** Get saved wallet accounts by [userWalletId] */
suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse?
}

View file

@ -23,6 +23,7 @@ dependencies {
implementation(projects.data.common)
/** Domain modules */
implementation(projects.domain.account)
implementation(projects.domain.onramp)
implementation(projects.domain.legacy)
implementation(projects.domain.card)

View file

@ -7,16 +7,19 @@ import com.tangem.blockchainsdk.utils.OLD_POLYGON_NAME
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.onramp.converters.HotCryptoCurrencyConverter
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.card.common.extensions.canHandleBlockchain
import com.tangem.domain.card.common.extensions.canHandleToken
import com.tangem.domain.models.wallet.UserWallet
@ -54,7 +57,9 @@ internal class DefaultHotCryptoRepository(
private val userWalletsStore: UserWalletsStore,
private val tangemTechApi: TangemTechApi,
private val appCurrencyResponseStore: AppCurrencyResponseStore,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val userTokensResponseStore: UserTokensResponseStore,
private val walletAccountsFetcher: WalletAccountsFetcher,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : HotCryptoRepository {
@ -100,7 +105,11 @@ internal class DefaultHotCryptoRepository(
private fun getWalletsWithTokensFlow(): Flow<Map<UserWallet, List<UserTokensResponse.Token>>> {
return userWalletsStore.userWallets.flatMapLatest { userWallets ->
val flows = userWallets.map { userWallet ->
userTokensResponseStore.get(userWalletId = userWallet.walletId)
if (accountsFeatureToggles.isFeatureEnabled) {
walletAccountsFetcher.get(userWalletId = userWallet.walletId).map { it.toUserTokensResponse() }
} else {
userTokensResponseStore.get(userWalletId = userWallet.walletId)
}
.map { userWallet to it?.tokens.orEmpty() }
}

View file

@ -3,6 +3,7 @@ package com.tangem.data.onramp.di
import com.squareup.moshi.Moshi
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.common.account.WalletAccountsFetcher
import com.tangem.data.onramp.DefaultHotCryptoRepository
import com.tangem.data.onramp.DefaultOnrampErrorResolver
import com.tangem.data.onramp.DefaultOnrampRepository
@ -28,6 +29,7 @@ import com.tangem.datasource.local.onramp.sepa.OnrampSepaAvailabilityStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.UserTokensResponseStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.onramp.repositories.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -109,6 +111,8 @@ internal object OnrampDataModule {
dispatchers: CoroutineDispatcherProvider,
analyticsEventHandler: AnalyticsEventHandler,
userTokensResponseStore: UserTokensResponseStore,
accountsFeatureToggles: AccountsFeatureToggles,
walletAccountsFetcher: WalletAccountsFetcher,
): HotCryptoRepository {
return DefaultHotCryptoRepository(
excludedBlockchains = excludedBlockchains,
@ -119,6 +123,8 @@ internal object OnrampDataModule {
dispatchers = dispatchers,
analyticsEventHandler = analyticsEventHandler,
userTokensResponseStore = userTokensResponseStore,
accountsFeatureToggles = accountsFeatureToggles,
walletAccountsFetcher = walletAccountsFetcher,
)
}

View file

@ -66,6 +66,11 @@ internal class DefaultPromoRepository(
PromoId.OnePlusOne -> {
val isActive = getOnePlusOnePromoBanner()?.isActive == true
isActive && shouldShow
}
PromoId.YieldPromo -> {
val isActive = getYieldPromoBanner(userWalletId)?.isActive == true
isActive && shouldShow
}
}
@ -79,6 +84,7 @@ internal class DefaultPromoRepository(
PromoId.VisaPresale -> flowOf(false)
PromoId.BlackFriday -> flowOf(false)
PromoId.OnePlusOne -> flowOf(false)
PromoId.YieldPromo -> flowOf(false)
}
}
@ -190,12 +196,22 @@ internal class DefaultPromoRepository(
}.getOrNull()
}
private suspend fun getYieldPromoBanner(userWalletId: UserWalletId): PromoBanner? {
return runCatching(dispatchers.io) {
val response = tangemApi.getPromoBannersV2(userWalletId.stringValue).getOrThrow()
val yieldPromotion = response.promotions.find { it.name == YIELD_PROMO_NAME }
?: return@runCatching null
promoBannerConverter.convert(yieldPromotion)
}.getOrNull()
}
private companion object {
const val SEPA_NAME = "sepa"
const val VISA_NAME = "visa-waitlist"
const val BLACK_FRIDAY_NAME = "black-friday"
const val MOONPAY_NAME = "moonpay"
const val ONE_PLUS_ONE_NAME = "one-plus-one"
const val YIELD_PROMO_NAME = "promo-yield"
const val STORIES_LOAD_DELAY = 1000L
}
}

View file

@ -31,4 +31,5 @@ enum class PromoId {
VisaPresale,
BlackFriday,
OnePlusOne,
YieldPromo,
}

View file

@ -33,6 +33,7 @@ class ShouldShowPromoWalletUseCase(
PromoId.VisaPresale,
PromoId.BlackFriday,
PromoId.OnePlusOne,
PromoId.YieldPromo,
-> true
PromoId.Sepa -> {
val walletFirstUsageDate = settingsRepository.getWalletFirstUsageDate()

View file

@ -60,5 +60,6 @@ sealed class PromoAnalyticsEvent(
Sepa("Sepa"),
BlackFriday("Black Friday"),
OnePlusOne("One-Plus-One"),
YieldPromo("Yield Promo"),
}
}

View file

@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable
sealed class NavigationAction {
data object Staking : NavigationAction()
data class YieldSupply(val isActive: Boolean) : NavigationAction()
data object CloreMigration : NavigationAction()
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.transaction.error
sealed class SignCloreMessageError {
data object WalletManagerNotFound : SignCloreMessageError()
data object MessageSigningNotSupported : SignCloreMessageError()
data class SigningFailed(val message: String) : SignCloreMessageError()
}

View file

@ -0,0 +1,58 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.blockchain.common.MessageSigner
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.common.CompletionResult
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.transaction.error.SignCloreMessageError
import com.tangem.domain.walletmanager.WalletManagersFacade
class SignCloreMessageUseCase(
private val walletManagersFacade: WalletManagersFacade,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val getHotWalletSigner: (UserWallet.Hot) -> TransactionSigner,
) {
suspend operator fun invoke(
userWallet: UserWallet,
currency: CryptoCurrency,
message: String,
): Either<SignCloreMessageError, String> {
val walletManager = walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
network = currency.network,
)
if (walletManager == null) {
return SignCloreMessageError.WalletManagerNotFound.left()
}
if (walletManager !is MessageSigner) {
return SignCloreMessageError.MessageSigningNotSupported.left()
}
val signer = when (userWallet) {
is UserWallet.Cold -> {
val card = userWallet.scanResponse.card
val isCardNotBackedUp = card.backupStatus?.isActive != true
cardSdkConfigRepository.getCommonSigner(
cardId = card.cardId.takeIf { isCardNotBackedUp },
twinKey = null,
)
}
is UserWallet.Hot -> getHotWalletSigner(userWallet)
}
return when (val result = walletManager.signMessage(message, signer)) {
is CompletionResult.Success -> result.data.right()
is CompletionResult.Failure -> SignCloreMessageError.SigningFailed(
message = result.error.message ?: "Unknown error",
).left()
}
}
}

View file

@ -1,6 +1,9 @@
package com.tangem.domain.wallets.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
import com.tangem.core.analytics.models.OneTimeAnalyticsEvent
sealed class Settings(
category: String = "Settings",
@ -9,4 +12,13 @@ sealed class Settings(
) : AnalyticsEvent(category, event, params) {
class ButtonManageTokens : Settings(event = "Button - Manage Tokens")
class ColdWalletAdded(
source: AnalyticsParam.ScreensSources?,
) : Settings(
event = "Cold Wallet Added",
params = mapOf(AnalyticsParam.SOURCE to (source?.value ?: "Unknown")),
), OneTimeAnalyticsEvent, AppsFlyerIncludedEvent {
override val oneTimeEventId: String = id
}
}

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.ACTION
import com.tangem.core.analytics.models.AnalyticsParam.Key.STATUS
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
sealed class WalletSettingsAnalyticEvents(
category: String = "Settings / Wallet Settings",
@ -166,6 +167,10 @@ sealed class WalletSettingsAnalyticEvents(
event = "Button - Start Upgrade",
)
class WalletUpgraded : WalletSettingsAnalyticEvents(
event = "Wallet Upgraded",
), AppsFlyerIncludedEvent
enum class RecoveryPhraseScreenAction(val value: String) {
Upgrade("Upgrade"),
Backup("Backup"),

View file

@ -3,9 +3,12 @@ package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.right
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.error.SaveWalletError
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.analytics.Settings
import com.tangem.domain.wallets.repository.WalletsRepository
/**
@ -16,13 +19,20 @@ import com.tangem.domain.wallets.repository.WalletsRepository
class SaveWalletUseCase(
private val userWalletsListRepository: UserWalletsListRepository,
private val walletsRepository: WalletsRepository,
private val analyticsEventHandler: AnalyticsEventHandler,
) {
suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either<SaveWalletError, Unit> {
suspend operator fun invoke(
userWallet: UserWallet,
canOverride: Boolean = false,
analyticsSource: AnalyticsParam.ScreensSources? = null,
): Either<SaveWalletError, Unit> {
return either {
val newUserWallet =
userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId }
val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride).bind()
val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride)
.onRight { trackColdWalletAddedIfNeeded(analyticsSource, it) }
.bind()
if (newUserWallet) {
when (userWallet) {
@ -50,4 +60,11 @@ class SaveWalletUseCase(
}
}
}
private suspend fun trackColdWalletAddedIfNeeded(source: AnalyticsParam.ScreensSources?, userWallet: UserWallet) {
val hasHotWallet = userWalletsListRepository.userWalletsSync().any { it is UserWallet.Hot }
if (hasHotWallet && userWallet is UserWallet.Cold) {
analyticsEventHandler.send(event = Settings.ColdWalletAdded(source))
}
}
}

View file

@ -50,7 +50,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
),
addNewWalletText = resourceReference(R.string.user_wallet_list_add_button),
isWalletSavingInProgress = true,
addNewWalletIconRes = R.drawable.ic_plus_24,
onAddNewWalletClick = {},
),
)

View file

@ -1,6 +1,5 @@
package com.tangem.features.details.entity
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.extensions.TextReference
@ -11,6 +10,5 @@ internal data class UserWalletListUM(
val userWallets: ImmutableList<UserWalletItemUM>,
val isWalletSavingInProgress: Boolean,
val addNewWalletText: TextReference,
@DrawableRes val addNewWalletIconRes: Int?,
val onAddNewWalletClick: () -> Unit,
)

View file

@ -55,7 +55,6 @@ internal class UserWalletListModel @Inject constructor(
isWalletSavingInProgress = false,
addNewWalletText = TextReference.EMPTY,
onAddNewWalletClick = ::onAddNewWalletClick,
addNewWalletIconRes = R.drawable.ic_plus_24,
),
)

View file

@ -1,9 +1,10 @@
package com.tangem.features.details.ui
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@ -11,10 +12,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.extensions.TextReference
@ -24,6 +28,7 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.details.component.UserWalletListComponent
import com.tangem.features.details.component.preview.PreviewUserWalletListComponent
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.impl.R
@Composable
internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) {
@ -42,7 +47,6 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M
text = state.addNewWalletText,
isInProgress = state.isWalletSavingInProgress,
onClick = state.onAddNewWalletClick,
icon = state.addNewWalletIconRes,
)
}
}
@ -50,7 +54,6 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M
@Composable
private fun AddWalletButton(
text: TextReference,
@DrawableRes icon: Int?,
isInProgress: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
@ -67,22 +70,30 @@ private fun AddWalletButton(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
if (icon != null) {
AnimatedContent(
modifier = Modifier.size(TangemTheme.dimens.size24),
targetState = isInProgress,
label = "Add wallet progress",
) { isInProgress ->
if (isInProgress) {
CircularProgressIndicator(
modifier = Modifier.size(TangemTheme.dimens.size24),
color = TangemTheme.colors.icon.accent,
)
} else {
AnimatedContent(
modifier = Modifier.size(TangemTheme.dimens.size36),
targetState = isInProgress,
label = "Add wallet progress",
) { isInProgress ->
if (isInProgress) {
CircularProgressIndicator(
modifier = Modifier
.size(TangemTheme.dimens.size24)
.padding(4.dp),
color = TangemTheme.colors.icon.accent,
)
} else {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(10.dp))
.background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f)),
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = icon),
modifier = Modifier.size(18.dp),
tint = TangemTheme.colors.icon.accent,
imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus_24),
contentDescription = null,
)
}

View file

@ -73,7 +73,12 @@ internal class UserWalletSaver @Inject constructor(
private suspend fun Raise<Error>.saveWallet(userWallet: UserWallet) {
fold(
block = { saveWalletUseCase(userWallet).bind() },
block = {
saveWalletUseCase(
userWallet = userWallet,
analyticsSource = AnalyticsParam.ScreensSources.Settings,
).bind()
},
recover = { error ->
when (error) {
is SaveWalletError.WalletAlreadySaved -> {

View file

@ -12,8 +12,7 @@ import kotlinx.serialization.Serializable
data class AvailableToAddData(
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet>,
) {
val isAvailableToAdd: Boolean
get() = availableToAddWallets.isNotEmpty()
val isAvailableToAdd: Boolean = availableToAddWallets.values.any { item -> item.isAvailableToAdd }
val isSinglePortfolio: Boolean
get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1
}
@ -23,7 +22,9 @@ data class AvailableToAddWallet(
val accounts: List<AccountStatus>,
val availableNetworks: Set<TokenMarketInfo.Network>,
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount>,
)
) {
val isAvailableToAdd: Boolean = availableToAddAccounts.values.any { item -> item.isAvailableToAdd }
}
@Serializable
data class AvailableToAddAccount(
@ -38,6 +39,8 @@ data class AvailableToAddAccount(
.filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } }
.toSet()
val isAvailableToAdd: Boolean = availableToAddNetworks.isNotEmpty()
val addedMarketNetworks: Set<TokenMarketInfo.Network> = availableNetworks
.filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } }
.toSet()

View file

@ -70,7 +70,6 @@ internal class AvailableToAddDataConverter @Inject constructor(
val availableToAddAccount = accountStatus.getAvailableToAddAccount(wallet) ?: return@mapNotNull null
accountStatus.account.accountId to availableToAddAccount
}
.filter { (_, account) -> account.availableToAddNetworks.isNotEmpty() }
.toMap()
return AvailableToAddWallet(
userWallet = wallet,

View file

@ -344,9 +344,10 @@ internal class AddToPortfolioModel @Inject constructor(
portfolioSelectorController.isEnabled.value = isEnabled@{ userWallet, accountStatus ->
val availableWallet = state.availableToAddData.availableToAddWallets[userWallet.walletId]
?: return@isEnabled false
val availableAccount =
val isAvailableAccount =
availableWallet.availableToAddAccounts[accountStatus.account.accountId]
return@isEnabled availableAccount != null
?.isAvailableToAdd == true
return@isEnabled isAvailableAccount
}
AddToPortfolioManager.State.Init,
AddToPortfolioManager.State.NothingToAdd,

View file

@ -25,15 +25,19 @@ internal class UpdateGlobalFeedStateTransformer(
prevState.news.newsUMState == NewsUMState.ERROR
val hasLoadingInCharts = loadingStatesByOrder.values.any { it }
val hasErrorInCharts = errorStatesByOrder.values.any { it != null }
val hasAllErrorInCharts = errorStatesByOrder.values.isNotEmpty() && errorStatesByOrder.values.all { it != null }
val hasErrorInMarketChart = errorStatesByOrder[SortByTypeUM.Rating] != null
val hasErrorInMarketPulseChart = errorStatesByOrder
.any { it.key != SortByTypeUM.Rating && it.value != null }
val areAllChartsLoading = loadingStatesByOrder.values.isNotEmpty() && loadingStatesByOrder.values.all { it }
val areAllChartsError = hasAllErrorInCharts || hasErrorInCharts && hasLoadingInCharts
val areAllChartsError = when {
hasErrorInMarketChart && hasErrorInMarketPulseChart -> true
hasErrorInMarketPulseChart && hasLoadingInCharts -> true
hasErrorInMarketChart && hasLoadingInCharts -> true
else -> false
}
val newGlobalState = when {
isNewsLoading && areAllChartsError -> GlobalFeedState.Loading
isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading
isNewsError && areAllChartsLoading -> GlobalFeedState.Loading
isNewsError && areAllChartsError -> {
if (previousGlobalState !is GlobalFeedState.Error) {
@ -43,18 +47,11 @@ internal class UpdateGlobalFeedStateTransformer(
onRetryClicked = onRetryClicked,
)
}
isNewsLoading && areAllChartsError -> GlobalFeedState.Loading
isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading
else -> GlobalFeedState.Content
}
return prevState.copy(
globalState = newGlobalState,
news = prevState.news.copy(
newsUMState = when {
isNewsError -> NewsUMState.ERROR
else -> NewsUMState.CONTENT
},
),
)
return prevState.copy(globalState = newGlobalState)
}
private fun sendErrorAnalytics() {

View file

@ -426,7 +426,7 @@ private fun Charts(
) {
BlockCard(
modifier = modifier,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.primary),
) {
Column(modifier = Modifier.fillMaxWidth()) {
when (marketChart) {

View file

@ -186,7 +186,10 @@ internal class HomeModel @Inject constructor(
return
}
saveWalletUseCase(userWallet).fold(
saveWalletUseCase(
userWallet = userWallet,
analyticsSource = AnalyticsParam.ScreensSources.Intro,
).fold(
ifLeft = {
delay(HIDE_PROGRESS_DELAY)
setLoading(false)

View file

@ -133,7 +133,10 @@ internal class CreateHardwareWalletModel @Inject constructor(
return
}
saveWalletUseCase(userWallet = userWallet).fold(
saveWalletUseCase(
userWallet = userWallet,
analyticsSource = AnalyticsParam.ScreensSources.AddNew,
).fold(
ifLeft = { saveWalletError ->
delay(HIDE_PROGRESS_DELAY)
setLoading(false)

View file

@ -12,8 +12,7 @@ import kotlinx.serialization.Serializable
internal data class AvailableToAddData(
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet>,
) {
val isAvailableToAdd: Boolean
get() = availableToAddWallets.isNotEmpty()
val isAvailableToAdd: Boolean = availableToAddWallets.values.any { item -> item.isAvailableToAdd }
val isSinglePortfolio: Boolean
get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1
}
@ -23,7 +22,9 @@ internal data class AvailableToAddWallet(
val accounts: List<AccountStatus>,
val availableNetworks: Set<TokenMarketInfo.Network>,
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount>,
)
) {
val isAvailableToAdd: Boolean = availableToAddAccounts.values.any { item -> item.isAvailableToAdd }
}
@Serializable
internal data class AvailableToAddAccount(
@ -31,6 +32,7 @@ internal data class AvailableToAddAccount(
val availableNetworks: Set<TokenMarketInfo.Network>,
val addedNetworks: Set<Network>,
) {
val isSingleNetwork: Boolean
get() = availableNetworks.size == 1
@ -38,6 +40,8 @@ internal data class AvailableToAddAccount(
.filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } }
.toSet()
val isAvailableToAdd: Boolean = availableToAddNetworks.isNotEmpty()
val addedMarketNetworks: Set<TokenMarketInfo.Network> = availableNetworks
.filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } }
.toSet()

View file

@ -70,7 +70,6 @@ internal class AvailableToAddDataConverter @Inject constructor(
val availableToAddAccount = accountStatus.getAvailableToAddAccount(wallet) ?: return@mapNotNull null
accountStatus.account.accountId to availableToAddAccount
}
.filter { (_, account) -> account.availableToAddNetworks.isNotEmpty() }
.toMap()
return AvailableToAddWallet(
userWallet = wallet,

View file

@ -334,9 +334,10 @@ internal class AddToPortfolioModel @Inject constructor(
portfolioSelectorController.isEnabled.value = isEnabled@{ userWallet, accountStatus ->
val availableWallet = state.availableToAddData.availableToAddWallets[userWallet.walletId]
?: return@isEnabled false
val availableAccount =
val isAvailableAccount =
availableWallet.availableToAddAccounts[accountStatus.account.accountId]
return@isEnabled availableAccount != null
?.isAvailableToAdd == true
return@isEnabled isAvailableAccount
}
AddToPortfolioManager.State.Init,
AddToPortfolioManager.State.NothingToAdd,

View file

@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.mo
import androidx.compose.runtime.Stable
import com.tangem.core.analytics.api.AnalyticsEventHandler
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
@ -50,17 +51,20 @@ internal class Wallet1ChooseOptionModel @Inject constructor(
val scanResponse = params.multiWalletState.value.currentScanResponse
val userWallet = createUserWallet(scanResponse)
saveWalletUseCase(userWallet, canOverride = true)
.onRight {
cardRepository.finishCardActivation(scanResponse.card.cardId)
saveWalletUseCase(
userWallet = userWallet,
canOverride = true,
analyticsSource = AnalyticsParam.ScreensSources.Onboarding,
).onRight {
cardRepository.finishCardActivation(scanResponse.card.cardId)
// save user wallet for manage tokens screen
params.multiWalletState.update {
it.copy(resultUserWallet = userWallet)
}
returnToParentFlow.emit(Unit)
// save user wallet for manage tokens screen
params.multiWalletState.update {
it.copy(resultUserWallet = userWallet)
}
returnToParentFlow.emit(Unit)
}
}
}

View file

@ -135,17 +135,20 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
val scanResponse = params.multiWalletState.value.currentScanResponse
val userWallet = createUserWallet(scanResponse)
saveWalletUseCase(userWallet, canOverride = true)
.onRight {
cardRepository.finishCardActivation(scanResponse.card.cardId)
saveWalletUseCase(
userWallet = userWallet,
canOverride = true,
analyticsSource = AnalyticsParam.ScreensSources.Onboarding,
).onRight {
cardRepository.finishCardActivation(scanResponse.card.cardId)
// save user wallet for manage tokens screen
params.multiWalletState.update {
it.copy(resultUserWallet = userWallet)
}
onDone.emit(Step.Done)
// save user wallet for manage tokens screen
params.multiWalletState.update {
it.copy(resultUserWallet = userWallet)
}
onDone.emit(Step.Done)
}
}
}

View file

@ -5,6 +5,7 @@ import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ByteArrayKey
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
@ -267,6 +268,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
scanResponse = scanResponse.updateScanResponseAfterBackup(),
),
canOverride = true,
analyticsSource = AnalyticsParam.ScreensSources.Onboarding,
)
userWalletCreated
}

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
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
@ -134,17 +135,20 @@ internal class MultiWalletUpgradeWalletModel @Inject constructor(
val scanResponse = params.multiWalletState.value.currentScanResponse
val userWallet = createUserWallet(scanResponse)
saveWalletUseCase(userWallet, canOverride = true)
.onRight {
cardRepository.finishCardActivation(scanResponse.card.cardId)
saveWalletUseCase(
userWallet,
canOverride = true,
analyticsSource = AnalyticsParam.ScreensSources.Onboarding,
).onRight {
cardRepository.finishCardActivation(scanResponse.card.cardId)
// save user wallet for manage tokens screen
params.multiWalletState.update {
it.copy(resultUserWallet = userWallet)
}
onDone.emit(Step.Done)
// save user wallet for manage tokens screen
params.multiWalletState.update {
it.copy(resultUserWallet = userWallet)
}
onDone.emit(Step.Done)
}
}
}

View file

@ -82,7 +82,11 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor(
private fun createWalletAndNavigateBackWithDone(scanResponse: ScanResponse) {
modelScope.launch {
val userWallet = createUserWallet(scanResponse)
saveWalletUseCase(userWallet, canOverride = true).onRight {
saveWalletUseCase(
userWallet,
canOverride = true,
analyticsSource = AnalyticsParam.ScreensSources.Onboarding,
).onRight {
params.onWalletCreated(userWallet)
}
_uiState.update {

View file

@ -300,6 +300,7 @@ internal class OnboardingTwinModel @Inject constructor(
saveWalletUseCase(
userWallet = userWallet,
canOverride = true,
analyticsSource = AnalyticsParam.ScreensSources.Onboarding,
).onLeft {
Timber.e("Unable to save user wallet: $it")
setLoading(false)
@ -329,6 +330,7 @@ internal class OnboardingTwinModel @Inject constructor(
saveWalletUseCase(
userWallet = userWallet,
canOverride = true,
analyticsSource = AnalyticsParam.ScreensSources.Onboarding,
).onLeft {
Timber.e("Unable to save user wallet: $it")
setLoading(false)

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
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
@ -173,7 +174,10 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
}
val userWallet = createUserWallet(params.scanResponse, newTokens)
saveWalletUseCase(userWallet)
saveWalletUseCase(
userWallet = userWallet,
analyticsSource = AnalyticsParam.ScreensSources.Onboarding,
)
visaAuthTokenStorage.remove(params.scanResponse.card.cardId)
otpStorage.removeOTP(params.scanResponse.card.cardId)

View file

@ -70,7 +70,6 @@ internal object SwapAmountQuoteUtils {
}
return copy(
isPrimaryButtonEnabled = updatedAmountField.amountField.isPrimaryButtonEnabled,
primaryAmount = updatedAmountField,
)
}

View file

@ -179,7 +179,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
val swapDirection: SwapDirection,
val isBalanceHidingFlow: StateFlow<Boolean>,
val primaryCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
val primaryFeePaidCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
val primaryFeePaidCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>, // doesn't change if select gasless fee
val accountFlow: StateFlow<Account.CryptoPortfolio?>,
val isAccountModeFlow: StateFlow<Boolean>,
val callback: ModelCallback,

View file

@ -22,10 +22,10 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.domain.express.models.ExpressProviderType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.isHotWallet
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection
import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase
@ -395,6 +395,8 @@ internal class SendWithSwapConfirmModel @Inject constructor(
private fun updateConfirmNotifications() {
modelScope.launch {
val feeCryptoCurrencyStatus =
feeUMV2?.feeExtraInfo?.feeCryptoCurrencyStatus ?: params.primaryFeePaidCurrencyStatusFlow.value
sendNotificationsUpdateTrigger.triggerUpdate(
data = NotificationData(
destinationAddress = when (val currency = primaryCurrencyStatus.currency) {
@ -407,7 +409,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
isIgnoreReduce = confirmData.isIgnoreReduce,
fee = confirmData.fee,
feeError = confirmData.feeError,
feeCryptoCurrencyStatus = params.primaryFeePaidCurrencyStatusFlow.value,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
),
)
swapNotificationsUpdateTrigger.triggerUpdate(

View file

@ -1536,9 +1536,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
swapAmount = amount,
quotesLoadedState = swapState,
isAllowedToSpend = isAllowedToSpend,
spenderAddress = requireNotNull(quoteModel.allowanceContract) {
"allowanceContract is required for DEX"
},
spenderAddress = quoteModel.allowanceContract,
)
if (state !is SwapState.QuotesLoadedState) return state
state.copy(
@ -2081,14 +2079,14 @@ internal class SwapInteractorImpl @AssistedInject constructor(
) ?: TxFeeState.Empty
}
@Suppress("LongParameterList", "LongMethod")
@Suppress("LongParameterList", "LongMethod", "CanBeNonNullable")
private suspend fun updatePermissionState(
networkId: String,
fromTokenStatus: CryptoCurrencyStatus,
fromAccount: Account.CryptoPortfolio?,
swapAmount: SwapAmount,
quotesLoadedState: SwapState.QuotesLoadedState,
spenderAddress: String,
spenderAddress: String?,
isAllowedToSpend: Boolean,
): SwapState {
val fromToken = fromTokenStatus.currency
@ -2111,7 +2109,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
}
// setting up amount for approve with given amount for swap [SwapApproveType.Limited]
val callData = SmartContractCallDataProviderFactory.getApprovalCallData(
spenderAddress = spenderAddress,
spenderAddress = requireNotNull(spenderAddress) { "spenderAddress cant be null" },
amount = swapAmount.value.convertToSdkAmount(fromTokenStatus),
blockchain = fromToken.network.toBlockchain(),
)

View file

@ -2,9 +2,7 @@ package com.tangem.feature.swap
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
@ -103,13 +101,10 @@ internal class DefaultSwapComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
if (sendFeatureToggles.isGaslessTransactionsEnabled) {
val fromCryptoCurrency = if (model.isOrderReversed) {
model.dataState.toCryptoCurrency
} else {
model.dataState.fromCryptoCurrency
}
val feePaidCryptoCurrency = model.dataState.feePaidCryptoCurrency
val amount = model.dataState.amount
val dataState by model.dataStateStateFlow.collectAsStateWithLifecycle()
val fromCryptoCurrency by remember { derivedStateOf { dataState.fromCryptoCurrency } }
val feePaidCryptoCurrency by remember { derivedStateOf { dataState.feePaidCryptoCurrency } }
val amount by remember { derivedStateOf { dataState.amount } }
LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, amount.isNullOrBlank()) {
if (amount.isNullOrBlank()) {

View file

@ -80,7 +80,6 @@ import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.models.AddToPortfolioRoute
import com.tangem.feature.swap.models.SwapCardState
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.UiActions
import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager
@ -200,7 +199,12 @@ internal class SwapModel @Inject constructor(
private val searchDebouncer = Debouncer()
private val singleTaskScheduler = SingleTaskScheduler<Map<SwapProvider, SwapState>>()
var dataState by mutableStateOf(SwapProcessDataState())
val dataStateStateFlow = MutableStateFlow(SwapProcessDataState())
var dataState
get() = dataStateStateFlow.value
set(value) {
dataStateStateFlow.value = value
}
var uiState: SwapStateHolder by mutableStateOf(
stateBuilder.createInitialLoadingState(
@ -220,9 +224,17 @@ internal class SwapModel @Inject constructor(
private val swapRouter: SwapRouter = SwapRouter(router = router)
private var userCountry: UserCountry? = null
private lateinit var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus
private var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null
private var toAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null
/**
* If accountsFeatureToggles is off OR user came from Tangem Pay -> fromAccountCurrencyStatus == null
* If accountsFeatureToggles is on AND user didn't come from Tangem Pay -> fromAccountCurrencyStatus != null
*
* Remove when accounts are integrated into Tangem Pay
*/
private val canUseFromAccountCurrencyStatus = accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null
private val isUserResolvableError: (SwapState) -> Boolean = { swapState ->
swapState is SwapState.SwapError &&
(
@ -288,7 +300,7 @@ internal class SwapModel @Inject constructor(
}
modelScope.launch(dispatchers.io) {
if (accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null) {
if (canUseFromAccountCurrencyStatus) {
isAccountsMode = isAccountsModeEnabledUseCase.invokeSync()
val fromAccountStatus = getAccountCurrencyStatusUseCase.invokeSync(
@ -579,11 +591,11 @@ internal class SwapModel @Inject constructor(
} else {
initialFromStatus to selectedCurrency
}
val (fromAccount, toAccount) = if (accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null) {
val (fromAccount, toAccount) = if (canUseFromAccountCurrencyStatus) {
if (isOrderReversed) {
selectedAccount to fromAccountCurrencyStatus.account
selectedAccount to requireNotNull(fromAccountCurrencyStatus).account
} else {
fromAccountCurrencyStatus.account to selectedAccount
requireNotNull(fromAccountCurrencyStatus).account to selectedAccount
}
} else {
null to null
@ -691,6 +703,7 @@ internal class SwapModel @Inject constructor(
toProvidersList: List<SwapProvider>,
updateFeeBlock: Boolean = true,
): PeriodicTask<Map<SwapProvider, SwapState>> {
var shouldUpdateFeeBlock = updateFeeBlock
return PeriodicTask(
delay = UPDATE_DELAY,
task = {
@ -726,8 +739,10 @@ internal class SwapModel @Inject constructor(
tokenSwapInfoForProviders = successStates.entries
.associate { it.key.providerId to it.value.toTokenInfo },
)
if (updateFeeBlock) {
if (shouldUpdateFeeBlock) {
modelScope.launch { feeSelectorReloadTrigger.triggerUpdate() }
} else {
shouldUpdateFeeBlock = true
}
} else {
feeSelectorRepository.state.value = FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true)
@ -1266,7 +1281,7 @@ internal class SwapModel @Inject constructor(
fromAccount = foundAccount
toToken = initialFromStatus
toAccount = if (accountsFeatureToggles.isFeatureEnabled) {
fromAccountCurrencyStatus.account
fromAccountCurrencyStatus?.account
} else {
null
}
@ -1284,7 +1299,7 @@ internal class SwapModel @Inject constructor(
} else {
fromToken = initialFromStatus
fromAccount = if (accountsFeatureToggles.isFeatureEnabled) {
fromAccountCurrencyStatus.account
fromAccountCurrencyStatus?.account
} else {
null
}
@ -2132,20 +2147,19 @@ internal class SwapModel @Inject constructor(
)
}
@Suppress("UnsafeCallOnNullableType")
inner class FeeSelectorRepository : SwapFeeSelectorBlockComponent.ModelRepositoryExtended {
override val state = MutableStateFlow<FeeSelectorUM>(FeeSelectorUM.Loading)
override val state = MutableStateFlow<FeeSelectorUM>(
FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true),
)
override suspend fun loadFeeExtended(
selectedToken: CryptoCurrencyStatus?,
): Either<GetFeeError, TransactionFeeExtended> {
val sendCardData =
uiState.sendCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError)
val receiveCardData =
uiState.receiveCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError)
val fromToken = sendCardData.token ?: return Either.Left(GetFeeError.UnknownError)
val toToken = receiveCardData.token ?: return Either.Left(GetFeeError.UnknownError)
val selectedProvider = dataState.selectedProvider ?: return Either.Left(GetFeeError.UnknownError)
val fromToken = dataState.fromCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError)
val toToken = dataState.toCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError)
val selectedProvider = dataStateStateFlow.first { it.selectedProvider != null }.selectedProvider!!
if (dataState.lastLoadedSwapStates[selectedProvider] !is SwapState.QuotesLoadedState) {
return Either.Left(GetFeeError.UnknownError)
@ -2201,13 +2215,9 @@ internal class SwapModel @Inject constructor(
}
override suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
val sendCardData =
uiState.sendCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError)
val receiveCardData =
uiState.receiveCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError)
val fromToken = sendCardData.token ?: return Either.Left(GetFeeError.UnknownError)
val toToken = receiveCardData.token ?: return Either.Left(GetFeeError.UnknownError)
val selectedProvider = dataState.selectedProvider ?: return Either.Left(GetFeeError.UnknownError)
val fromToken = dataState.fromCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError)
val toToken = dataState.toCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError)
val selectedProvider = dataStateStateFlow.first { it.selectedProvider != null }.selectedProvider!!
if (dataState.lastLoadedSwapStates[selectedProvider] !is SwapState.QuotesLoadedState) {
return Either.Left(GetFeeError.UnknownError)

View file

@ -16,9 +16,7 @@ interface TangemPayOnboardingComponent : ComposableContentComponent {
val userWalletId: UserWalletId,
) : Params()
data class FromBannerOnMain(
val userWalletId: UserWalletId,
) : Params()
data object FromBannerOnMain : Params()
data object FromBannerInSettings : Params()
}

View file

@ -131,13 +131,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
is TangemPayOnboardingComponent.Params.ContinueOnboarding -> {
checkCustomerInfo(params.userWalletId)
}
is TangemPayOnboardingComponent.Params.FromBannerOnMain -> {
if (eligibleWalletsIds.any { walletId -> walletId == params.userWalletId }) {
checkCustomerInfo(userWalletId = params.userWalletId)
} else {
openWalletSelectorIfNeeds(eligibleWalletsIds)
}
}
is TangemPayOnboardingComponent.Params.FromBannerOnMain,
is TangemPayOnboardingComponent.Params.Deeplink,
is TangemPayOnboardingComponent.Params.FromBannerInSettings,
-> {

View file

@ -48,6 +48,7 @@ dependencies {
/** Core modules */
implementation(projects.common.routing)
implementation(projects.core.navigation)
implementation(projects.core.res)
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.analytics)

View file

@ -0,0 +1,113 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.model
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.transaction.error.SignCloreMessageError
import com.tangem.domain.transaction.usecase.SignCloreMessageUseCase
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
// TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY])
@Suppress("LongParameterList")
internal class CloreMigrationModel(
private val stateFactory: TokenDetailsStateFactory,
private val signCloreMessageUseCase: SignCloreMessageUseCase,
private val clipboardManager: ClipboardManager,
private val uiMessageSender: UiMessageSender,
private val router: InnerTokenDetailsRouter,
private val userWallet: UserWallet,
private val cryptoCurrency: CryptoCurrency,
private val coroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatcherProvider,
private val onStateUpdate: (TokenDetailsState) -> Unit,
) {
private val state = MutableStateFlow(CloreMigrationState())
fun onCloreMigrationClick() {
state.value = CloreMigrationState()
updateBottomSheet()
}
fun onCloreSignMessage(message: String) {
if (message.isBlank()) return
state.value = state.value.copy(message = message)
updateBottomSheet(isSigningInProgress = true)
coroutineScope.launch(dispatchers.main) {
signCloreMessageUseCase(
userWallet = userWallet,
currency = cryptoCurrency,
message = message,
).fold(
ifLeft = { error ->
val errorMessage = when (error) {
is SignCloreMessageError.SigningFailed -> stringReference(error.message)
SignCloreMessageError.MessageSigningNotSupported ->
resourceReference(R.string.warning_clore_migration_error_signing_not_supported)
SignCloreMessageError.WalletManagerNotFound ->
resourceReference(R.string.warning_clore_migration_error_wallet_manager_not_found)
}
state.value = state.value.copy(signature = "")
updateBottomSheet()
uiMessageSender.send(SnackbarMessage(errorMessage))
},
ifRight = { signature ->
state.value = state.value.copy(signature = signature)
onStateUpdate(stateFactory.getStateWithUpdatedCloreMigrationSignature(signature))
},
)
}
}
fun onOpenCloreClaimPortal() {
router.openUrl(CLAIM_PORTAL_URL)
}
private fun updateBottomSheet(isSigningInProgress: Boolean = false) {
val currentState = state.value
onStateUpdate(
stateFactory.getStateWithCloreMigrationBottomSheet(
message = currentState.message,
signature = currentState.signature,
isSigningInProgress = isSigningInProgress,
onMessageChange = { newMessage ->
state.value = state.value.copy(message = newMessage)
updateBottomSheet()
},
onSignClick = { onCloreSignMessage(state.value.message) },
onCopyClick = {
val signature = state.value.signature
if (signature.isNotBlank()) {
clipboardManager.setText(text = signature, isSensitive = false)
uiMessageSender.send(
SnackbarMessage(resourceReference(R.string.wallet_notification_address_copied)),
)
}
},
onOpenPortalClick = { onOpenCloreClaimPortal() },
),
)
}
private companion object {
const val CLAIM_PORTAL_URL = "https://claim-portal.clore.ai/"
}
}
private data class CloreMigrationState(
val message: String = "",
val signature: String = "",
)

View file

@ -64,7 +64,26 @@ interface TokenDetailsClickIntents {
fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig)
fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency)
fun onOpenUrlClick(url: String)
fun onConfirmDisposeExpressStatus()
fun onDisposeExpressStatus()
fun onYieldInfoClick()
// region Clore migration
// TODO: Remove after 2025-04-01 when Clore migration ends ([REDACTED_TASK_KEY])
fun onCloreMigrationClick()
fun onCloreSignMessage(message: String)
fun onOpenCloreClaimPortal()
// endregion Clore migration
}
interface ExpressTransactionsClickIntents {
@ -146,4 +165,23 @@ internal class EmptyTokenDetailsClickIntents : TokenDetailsClickIntents {
/* no op */
return null
}
override fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency) { /* no op */ }
override fun onOpenUrlClick(url: String) { /* no op */ }
override fun onConfirmDisposeExpressStatus() { /* no op */ }
override fun onDisposeExpressStatus() { /* no op */ }
// region Clore migration
// TODO: Remove after 2025-04-01 when Clore migration ends ([REDACTED_TASK_KEY])
override fun onCloreMigrationClick() { /* no op */ }
override fun onCloreSignMessage(message: String) { /* no op */ }
override fun onOpenCloreClaimPortal() { /* no op */ }
// endregion Clore migration
}

View file

@ -152,6 +152,7 @@ internal class TokenDetailsModel @Inject constructor(
private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val yieldSupplyGetRewardsBalanceUseCase: YieldSupplyGetRewardsBalanceUseCase,
private val signCloreMessageUseCase: SignCloreMessageUseCase,
) : Model(),
TokenDetailsClickIntents,
ExpressTransactionsClickIntents,
@ -161,7 +162,8 @@ internal class TokenDetailsModel @Inject constructor(
private val userWalletId: UserWalletId = params.userWalletId
private val cryptoCurrency: CryptoCurrency = params.currency
private val userWallet: UserWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: error("UserWallet not found")
private val userWallet: UserWallet = getUserWalletUseCase(userWalletId).getOrNull()
?: error("UserWallet not found")
private val marketPriceJobHolder = JobHolder()
private val refreshStateJobHolder = JobHolder()
@ -194,6 +196,27 @@ internal class TokenDetailsModel @Inject constructor(
yieldSupplyFeatureToggles = yieldSupplyFeatureToggles,
)
private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency))
val uiState: StateFlow<TokenDetailsState> = internalUiState
// region Clore migration
// TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY])
private val cloreMigrationModel by lazy(mode = LazyThreadSafetyMode.NONE) {
CloreMigrationModel(
stateFactory = stateFactory,
signCloreMessageUseCase = signCloreMessageUseCase,
clipboardManager = clipboardManager,
uiMessageSender = uiMessageSender,
router = router,
userWallet = userWallet,
cryptoCurrency = cryptoCurrency,
coroutineScope = modelScope,
dispatchers = dispatchers,
onStateUpdate = { internalUiState.value = it },
)
}
// endregion
private val expressStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
expressStatusFactory.create(
clickIntents = this,
@ -216,9 +239,6 @@ internal class TokenDetailsModel @Inject constructor(
TokenDetailsCurrencyStatusAnalyticsSender(analyticsEventsHandler)
}
private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency))
val uiState: StateFlow<TokenDetailsState> = internalUiState
init {
updateTopBarMenu()
initButtons()
@ -667,9 +687,8 @@ internal class TokenDetailsModel @Inject constructor(
vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click)
clipboardManager.setText(text = extendedKey, isSensitive = true)
uiMessageSender.send(
message = SnackbarMessage(message = resourceReference(R.string.wallet_notification_address_copied)),
)
val message = resourceReference(R.string.wallet_notification_address_copied)
uiMessageSender.send(message = SnackbarMessage(message = message))
}
}
}
@ -786,7 +805,8 @@ internal class TokenDetailsModel @Inject constructor(
modelScope.launch(dispatchers.main) {
when (val addresses = currencyStatus.value.networkAddress) {
is NetworkAddress.Selectable -> {
internalUiState.value = stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses)
internalUiState.value =
stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses)
}
is NetworkAddress.Single -> {
router.openUrl(
@ -1256,11 +1276,26 @@ internal class TokenDetailsModel @Inject constructor(
}
private fun handleNavigationParam() {
if (params.navigationAction is NavigationAction.Staking) {
openStaking()
when (params.navigationAction) {
is NavigationAction.Staking -> openStaking()
is NavigationAction.CloreMigration -> onCloreMigrationClick()
is NavigationAction.YieldSupply,
null,
-> Unit
}
}
// region Clore migration
// TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY])
override fun onCloreMigrationClick() = cloreMigrationModel.onCloreMigrationClick()
override fun onCloreSignMessage(message: String) = cloreMigrationModel.onCloreSignMessage(message)
override fun onOpenCloreClaimPortal() = cloreMigrationModel.onOpenCloreClaimPortal()
// endregion Clore migration
private companion object {
const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L
}

View file

@ -258,9 +258,15 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
subtitle = resourceReference(id = R.string.warning_matic_migration_message),
)
data object MigrationClore : Warning(
data class MigrationClore(
private val onMigrationClick: () -> Unit,
) : Warning(
title = resourceReference(id = R.string.warning_clore_migration_title),
subtitle = resourceReference(id = R.string.warning_clore_migration_message),
subtitle = resourceReference(id = R.string.warning_clore_migration_description),
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(id = R.string.warning_clore_migration_button),
onClick = onMigrationClick,
),
)
data object UsedOutdatedData : TokenDetailsNotification(

View file

@ -155,7 +155,9 @@ internal class TokenDetailsNotificationConverter(
},
)
is CryptoCurrencyWarning.MigrationMaticToPol -> MigrationMaticToPol
is CryptoCurrencyWarning.MigrationClore -> MigrationClore
is CryptoCurrencyWarning.MigrationClore -> MigrationClore(
onMigrationClick = clickIntents::onCloreMigrationClick,
)
is CryptoCurrencyWarning.UsedOutdatedDataWarning -> UsedOutdatedData
}
}

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig
import com.tangem.common.ui.tokens.getUnavailabilityReasonText
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import com.tangem.core.ui.extensions.TextReference
@ -349,4 +350,62 @@ internal class TokenDetailsStateFactory(
}.toImmutableList(),
)
}
// region Clore migration
// TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY])
fun getStateWithCloreMigrationBottomSheet(
message: String,
signature: String,
isSigningInProgress: Boolean,
onMessageChange: (String) -> Unit,
onSignClick: () -> Unit,
onCopyClick: () -> Unit,
onOpenPortalClick: () -> Unit,
): TokenDetailsState {
return currentStateProvider().copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = expressTransactionsClickIntents::onDismissBottomSheet,
content = CloreMigrationBottomSheetConfig(
message = message,
signature = signature,
isSigningInProgress = isSigningInProgress,
onMessageChange = onMessageChange,
onSignClick = onSignClick,
onCopyClick = onCopyClick,
onOpenPortalClick = onOpenPortalClick,
),
),
)
}
fun getStateWithUpdatedCloreMigrationSignature(signature: String): TokenDetailsState {
val state = currentStateProvider()
val bottomSheetConfig = state.bottomSheetConfig ?: return state
val content = bottomSheetConfig.content as? CloreMigrationBottomSheetConfig ?: return state
return state.copy(
bottomSheetConfig = bottomSheetConfig.copy(
content = content.copy(
signature = signature,
isSigningInProgress = false,
),
),
)
}
fun getStateWithCloreMigrationSigning(): TokenDetailsState {
val state = currentStateProvider()
val bottomSheetConfig = state.bottomSheetConfig ?: return state
val content = bottomSheetConfig.content as? CloreMigrationBottomSheetConfig ?: return state
return state.copy(
bottomSheetConfig = bottomSheetConfig.copy(
content = content.copy(isSigningInProgress = true),
),
)
}
// endregion Clore migration
}

View file

@ -33,6 +33,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
@ -173,6 +175,9 @@ internal fun TokenDetailsScreen(
is ExpressStatusBottomSheetConfig -> {
ExpressStatusBottomSheet(config = config)
}
is CloreMigrationBottomSheetConfig -> {
CloreMigrationBottomSheet(config = config)
}
}
}
}

View file

@ -0,0 +1,220 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore
import android.content.res.Configuration
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R as CoreR
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tokendetails.impl.R
@Composable
internal fun CloreMigrationBottomSheet(config: TangemBottomSheetConfig) {
TangemModalBottomSheet<CloreMigrationBottomSheetConfig>(
config = config,
title = {
TangemModalBottomSheetTitle(
endIconRes = CoreR.drawable.ic_close_24,
onEndClick = config.onDismissRequest,
)
},
) { content: CloreMigrationBottomSheetConfig ->
CloreMigrationBottomSheetContent(content = content)
}
}
@Composable
private fun CloreMigrationBottomSheetContent(content: CloreMigrationBottomSheetConfig) {
Column(
modifier = Modifier
.padding(bottom = 16.dp)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
) {
Text(
text = stringResourceSafe(R.string.warning_clore_migration_sheet_title),
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.h3,
)
Text(
text = stringResourceSafe(R.string.warning_clore_migration_sheet_description),
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
style = TangemTheme.typography.body2,
modifier = Modifier.padding(horizontal = 8.dp),
)
Spacer(modifier = Modifier.height(8.dp))
MessageField(
placeholder = stringResourceSafe(R.string.warning_clore_migration_message_label),
value = content.message,
onValueChange = content.onMessageChange,
buttonText = stringResourceSafe(R.string.warning_clore_migration_sign_button),
onButtonClick = content.onSignClick,
isButtonEnabled = content.message.isNotBlank() && !content.isSigningInProgress,
isLoading = content.isSigningInProgress,
)
SignatureField(
placeholder = stringResourceSafe(R.string.warning_clore_migration_signature_label),
value = content.signature,
onCopyClick = content.onCopyClick,
)
Spacer(modifier = Modifier.height(8.dp))
PrimaryButton(
text = stringResourceSafe(R.string.warning_clore_migration_open_portal_button),
onClick = content.onOpenPortalClick,
modifier = Modifier.fillMaxWidth(),
)
}
}
@Suppress("LongParameterList")
@Composable
private fun MessageField(
placeholder: String,
value: String,
onValueChange: (String) -> Unit,
buttonText: String,
onButtonClick: () -> Unit,
isButtonEnabled: Boolean,
isLoading: Boolean,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.border(
width = 1.dp,
color = TangemTheme.colors.stroke.primary,
shape = TangemTheme.shapes.roundedCornersSmall,
)
.heightIn(min = 56.dp)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
SimpleTextField(
value = value,
onValueChange = onValueChange,
placeholder = TextReference.Str(placeholder),
modifier = Modifier.weight(1f),
singleLine = false,
)
Spacer(modifier = Modifier.width(8.dp))
SecondarySmallButton(
config = SmallButtonConfig(
text = TextReference.Str(buttonText),
onClick = onButtonClick,
isEnabled = isButtonEnabled,
isLoading = isLoading,
),
)
}
}
@Suppress("FunctionSignature")
@Composable
private fun SignatureField(placeholder: String, value: String, onCopyClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.border(
width = 1.dp,
color = TangemTheme.colors.stroke.primary,
shape = TangemTheme.shapes.roundedCornersSmall,
)
.heightIn(min = 56.dp)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
SimpleTextField(
value = value,
onValueChange = {},
placeholder = TextReference.Str(placeholder),
modifier = Modifier.weight(1f),
readOnly = true,
singleLine = false,
)
Spacer(modifier = Modifier.width(8.dp))
SecondarySmallButton(
config = SmallButtonConfig(
text = TextReference.Res(R.string.warning_clore_migration_copy_button),
onClick = onCopyClick,
isEnabled = value.isNotBlank(),
),
)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_CloreMigrationBottomSheet() {
TangemThemePreview {
val config = TangemBottomSheetConfig(
isShown = true,
content = CloreMigrationBottomSheetConfig(
message = "Claim request for CLORE tokens to Ethereum address " +
"0x742d35Cc6634C0532925a3b844Bc9e7595f60126 from CEsMERNgBVPo9Dkc99pRDMPD3mxwPMxHZi",
signature = "H4sIAAAAAAAEAGNgGAWjYBSMglEwCkYBHQEA",
isSigningInProgress = false,
onMessageChange = {},
onSignClick = {},
onCopyClick = {},
onOpenPortalClick = {},
),
onDismissRequest = {},
)
CloreMigrationBottomSheet(config)
}
}
@Composable
@Preview(showBackground = true, widthDp = 360)
private fun Preview_CloreMigrationBottomSheet_Empty() {
TangemThemePreview {
val config = TangemBottomSheetConfig(
isShown = true,
content = CloreMigrationBottomSheetConfig(
message = "",
signature = "",
isSigningInProgress = false,
onMessageChange = {},
onSignClick = {},
onCopyClick = {},
onOpenPortalClick = {},
),
onDismissRequest = {},
)
CloreMigrationBottomSheet(config)
}
}
// endregion Preview

View file

@ -0,0 +1,15 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
@Immutable
data class CloreMigrationBottomSheetConfig(
val message: String = "",
val signature: String = "",
val isSigningInProgress: Boolean = false,
val onMessageChange: (String) -> Unit,
val onSignClick: () -> Unit,
val onCopyClick: () -> Unit,
val onOpenPortalClick: () -> Unit,
) : TangemBottomSheetConfigContent

View file

@ -243,7 +243,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
modelScope.launch {
val isEligible = tangemPayEligibilityManager.getTangemPayAvailability()
if (isEligible) {
router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain(userWalletId))
router.openTangemPayOnboarding(mode = AppRoute.TangemPayOnboarding.Mode.FromBannerOnMain)
} else {
stateHolder.update(transformer = TangemPayHideOnboardingStateTransformer(userWalletId))
}

View file

@ -165,6 +165,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
cryptoCurrencyStatus = currencyStatus,
apy = apy,
)
is NavigationAction.CloreMigration -> Unit
}
}
@ -239,6 +240,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
},
)
}
is NavigationAction.CloreMigration -> return
}
analyticsEventHandler.send(event)
}

View file

@ -18,6 +18,7 @@ import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.requireColdWallet
@ -35,6 +36,7 @@ import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
@ -60,6 +62,8 @@ internal interface WalletWarningsClickIntents {
fun onAddBackupCardClick()
fun onCloreMigrationClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onCloseAlreadySignedHashesWarningClick()
fun onGenerateMissedAddressesClick(missedAddressCurrencies: List<CryptoCurrency>)
@ -95,6 +99,8 @@ internal interface WalletWarningsClickIntents {
fun onDenyPermissions()
fun onFinishWalletActivationClick(isBackupExists: Boolean)
fun onYieldPromoTermsAndConditionsClick()
}
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@ -135,6 +141,16 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
prepareAndStartOnboardingProcess()
}
override fun onCloreMigrationClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
val userWallet = getSelectedUserWallet() ?: return
router.openTokenDetails(
userWalletId = userWallet.walletId,
currencyStatus = cryptoCurrencyStatus,
navigationAction = NavigationAction.CloreMigration,
)
}
private fun prepareAndStartOnboardingProcess() {
modelScope.launch(dispatchers.main) {
getSelectedUserWallet()?.requireColdWallet()?.let {
@ -271,6 +287,11 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
program = Program.OnePlusOne,
action = PromotionBannerClicked.BannerAction.Closed(),
)
PromoId.YieldPromo -> PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.YieldPromo,
action = PromotionBannerClicked.BannerAction.Closed(),
)
},
)
@ -328,6 +349,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
)
urlOpener.openUrl(ONE_PLUS_ONE_PROMO_LINK)
}
PromoId.YieldPromo -> Unit // banner is not clickable, only terms and conditions button
}
}
@ -545,6 +567,17 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
}
override fun onYieldPromoTermsAndConditionsClick() {
analyticsEventHandler.send(
PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = Program.YieldPromo,
action = PromotionBannerClicked.BannerAction.Clicked(),
),
)
urlOpener.openUrl(YIELD_PROMO_TERMS_LINK)
}
private companion object {
const val VISA_PROMO_LINK = "https://tangem.com/en/cardwaitlist/?utm_source=tangem-app-banner" +
"&utm_medium=banner" +
@ -559,5 +592,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
"&utm_source=tangem-app-banner" +
"&utm_medium=banner" +
"&utm_campaign=BOGO50"
const val YIELD_PROMO_TERMS_LINK = "https://tangem.com/docs/yield-mode-toc.html"
}
}

View file

@ -59,14 +59,15 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
init {
analyticsEventsHandler.send(PromoActivationAnalytics.PromoDeepLinkActivationStart())
saveAndBindRefcode()
val promoCode = queryParams[PROMO_CODE_KEY].orEmpty()
if (promoCode.isEmpty()) {
showAlert(InvalidPromoCode)
} else {
findSelectedWallet(promoCode)
}
saveAndBindRefcode()
}
private fun findSelectedWallet(promoCode: String) {
@ -183,14 +184,19 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor(
message = message,
dismissOnFirstAction = true,
firstActionBuilder = {
okAction { }
okAction {
uiMessageSender.send(GlobalLoadingMessage(false))
}
},
),
)
}
@Suppress("NullableToStringCall")
private fun saveAndBindRefcode() {
scope.launch(dispatchers.default) {
Timber.i("saveAndBindRefcode: refcode = $refcode, campaign = $campaign")
if (!refcode.isNullOrBlank()) {
val conversionData = AppsFlyerConversionData(refcode = refcode, campaign = campaign)

View file

@ -219,9 +219,8 @@ private fun LazyItemScope.DraggableItem(
state = item.tokenItemState,
isCollapsable = false,
composables = null,
modifier = modifierWithBackground
.padding(top = 8.dp)
.fillMaxWidth(),
isBalanceHidden = isBalanceHidden,
modifier = modifierWithBackground.fillMaxWidth(),
)
}
}

View file

@ -72,7 +72,7 @@ sealed class WalletScreenAnalyticsEvent {
}
put("Wallet Type", seedPhrase)
},
)
), AppsFlyerIncludedEvent
data class NoticeFinishActivation(
private val activationState: ActivationState,

View file

@ -66,6 +66,10 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
source = AnalyticsParam.ScreensSources.Main,
program = Program.OnePlusOne,
)
is WalletNotification.YieldPromo -> NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
program = Program.YieldPromo,
)
is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo()
is WalletNotification.VisaPresalePromo -> VisaWaitlistPromo()
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]
@ -76,6 +80,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.UsedOutdatedData,
is WalletNotification.UnlockVisaAccess,
is WalletNotification.Warning.YeildSupplyApprove, // TODO apply correct event
is WalletNotification.CloreMigration,
-> null
is WalletNotification.FinishWalletActivation -> {
val activationState = if (warning.isBackupExists) {

View file

@ -33,6 +33,7 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.extensions.addIf
import com.tangem.utils.extensions.isPositive
import com.tangem.utils.extensions.orZero
@ -95,6 +96,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
notificationsRepository.getShouldShowNotification(NotificationId.EnablePushesReminderNotification.key)
.distinctUntilChanged(),
getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(),
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo)
.distinctUntilChanged(),
) { array -> array }
.combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) }
.map { array ->
@ -107,6 +110,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val shouldShowOnePlusOnePromo = array[4] as Boolean
val shouldShowEnablePushesReminderNotification = array[5] as Boolean
val shouldAccessCodeSkipped = array[6] as Boolean
val shouldShowYieldPromo = array[7] as Boolean
buildList {
addUsedOutdatedDataNotification(totalFiatBalance)
@ -122,6 +126,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
addOnePlusOnePromoNotification(clickIntents, shouldShowOnePlusOnePromo)
addYieldPromoNotification(clickIntents, shouldShowYieldPromo)
addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents)
addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents)
@ -286,6 +292,19 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun MutableList<WalletNotification>.addYieldPromoNotification(
clickIntents: WalletClickIntents,
shouldShowPromo: Boolean,
) {
addIf(
element = WalletNotification.YieldPromo(
onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.YieldPromo) },
onTermsAndConditionsClick = { clickIntents.onYieldPromoTermsAndConditionsClick() },
),
condition = shouldShowPromo,
)
}
// private fun MutableList<WalletNotification>.addYieldSupplyNotifications(
// flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
// ) {
@ -317,6 +336,29 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
element = WalletNotification.Warning.SomeNetworksUnreachable,
condition = flattenCurrencies.hasUnreachableNetworks(),
)
addCloreMigrationNotification(flattenCurrencies, clickIntents)
}
private fun MutableList<WalletNotification>.addCloreMigrationNotification(
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
clickIntents: WalletClickIntents,
) {
val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return
add(
WalletNotification.CloreMigration(
onStartMigrationClick = { clickIntents.onCloreMigrationClick(cloreCurrency) },
),
)
}
private fun Lce<TokenListError, List<CryptoCurrencyStatus>>.findCloreCurrency(): CryptoCurrencyStatus? {
val currencies = getOrNull(isPartialContentAccepted = true) ?: return null
return currencies.find { currencyStatus ->
BlockchainUtils.isClore(currencyStatus.currency.network.rawId)
}
}
private fun MutableList<WalletNotification>.addPushReminderNotification(

View file

@ -396,6 +396,23 @@ sealed class WalletNotification(val config: NotificationConfig) {
),
)
data class YieldPromo(
val onCloseClick: () -> Unit,
val onTermsAndConditionsClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.notification_yield_promo_title),
subtitle = resourceReference(R.string.notification_yield_promo_text),
iconResId = R.drawable.ic_yield_promo_36,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.notification_yield_promo_button),
onClick = onTermsAndConditionsClick,
),
iconSize = 36.dp,
),
)
data class PushNotifications(
val onCloseClick: () -> Unit,
val onEnabledClick: () -> Unit,
@ -414,4 +431,18 @@ sealed class WalletNotification(val config: NotificationConfig) {
iconSize = 54.dp,
),
)
data class CloreMigration(
val onStartMigrationClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_title),
subtitle = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_description),
iconResId = R.drawable.img_attention_20,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_button),
onClick = onStartMigrationClick,
),
),
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
import androidx.compose.animation.*
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
@ -141,7 +142,7 @@ private fun LazyListScope.portfolioItem(
lastIndexProxy = lastIndex
return@LaunchedEffect
}
delay(timeMillis = minOf(50 * tokens.size, 350).toLong())
delay(timeMillis = minOf(50 * tokens.size, 250).toLong())
lastIndexProxy = 0
}
@ -179,13 +180,13 @@ private fun SlideInItemVisibility(
enter = fadeIn(
tween(100, delayMillis = delayEnter),
) + expandVertically(
tween(100, delayMillis = delayEnter),
tween(100, delayMillis = delayEnter, easing = FastOutLinearInEasing),
expandFrom = Alignment.Top,
),
exit = fadeOut(
tween(100, delayMillis = delayExit),
) + shrinkVertically(
tween(100, delayMillis = delayExit),
tween(100, delayMillis = delayExit, easing = FastOutLinearInEasing),
shrinkTowards = Alignment.Top,
),
) {

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="36"
android:viewportHeight="36">
<path
android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
android:strokeAlpha="0.1"
android:fillColor="#0099FF"
android:fillAlpha="0.1"/>
<path
android:pathData="M18.057,7.745C20.247,7.745 21.968,7.745 23.312,7.926C24.688,8.111 25.781,8.498 26.639,9.356C27.497,10.214 27.885,11.307 28.07,12.683C28.25,14.027 28.25,15.748 28.25,17.938V18.052C28.25,20.243 28.25,21.964 28.07,23.307C27.885,24.684 27.497,25.776 26.639,26.635C25.781,27.493 24.688,27.879 23.312,28.064C21.968,28.245 20.247,28.245 18.057,28.245H17.943C15.752,28.245 14.031,28.245 12.688,28.064C11.311,27.879 10.219,27.493 9.361,26.635C8.502,25.776 8.116,24.684 7.931,23.307C7.75,21.964 7.75,20.243 7.75,18.052V17.938C7.75,15.748 7.75,14.027 7.931,12.683C8.116,11.307 8.503,10.214 9.361,9.356C10.219,8.498 11.311,8.111 12.688,7.926C14.031,7.745 15.752,7.745 17.943,7.745H18.057ZM13,21.248C12.586,21.248 12.25,21.584 12.25,21.998V23.998C12.25,24.412 12.586,24.748 13,24.748C13.414,24.748 13.75,24.412 13.75,23.998V21.998C13.75,21.584 13.414,21.248 13,21.248ZM18,20.247C17.586,20.247 17.25,20.583 17.25,20.997V23.997C17.25,24.411 17.586,24.747 18,24.747C18.414,24.747 18.75,24.411 18.75,23.997V20.997C18.75,20.583 18.414,20.247 18,20.247ZM23,18.245C22.586,18.245 22.25,18.581 22.25,18.995V23.995C22.25,24.409 22.586,24.745 23,24.745C23.414,24.745 23.75,24.409 23.75,23.995V18.995C23.75,18.581 23.414,18.245 23,18.245ZM21.859,11.247C21.654,11.256 21.416,11.295 21.179,11.335C21.168,11.337 21.157,11.339 21.146,11.341L19.878,11.55C19.47,11.617 19.193,12.002 19.26,12.411C19.327,12.82 19.714,13.097 20.122,13.029L20.846,12.91C18.963,15.552 15.953,16.745 12,16.745C11.586,16.745 11.25,17.081 11.25,17.495C11.25,17.909 11.586,18.245 12,18.245C16.322,18.245 19.898,16.9 22.132,13.689L22.279,14.201C22.393,14.599 22.808,14.829 23.206,14.716C23.604,14.601 23.835,14.186 23.721,13.788L23.403,12.677C23.399,12.666 23.396,12.655 23.393,12.643C23.335,12.44 23.271,12.218 23.189,12.036C23.088,11.814 22.93,11.576 22.645,11.414C22.369,11.257 22.09,11.236 21.859,11.247Z"
android:fillColor="#0099FF"
android:fillType="evenOdd"/>
</vector>

View file

@ -197,18 +197,20 @@ internal class WelcomeModel @Inject constructor(
onSuccess = { scanResponse ->
val userWallet =
coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() ?: return@scan
saveWalletUseCase.invoke(userWallet)
.onLeft { error ->
if (error is SaveWalletError.WalletAlreadySaved) {
userWalletsListRepository.unlock(
userWallet.walletId,
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse),
).onRight {
userWalletsListRepository.select(userWallet.walletId)
router.replaceAll(AppRoute.Wallet)
}
saveWalletUseCase.invoke(
userWallet = userWallet,
analyticsSource = AnalyticsParam.ScreensSources.SignIn,
).onLeft { error ->
if (error is SaveWalletError.WalletAlreadySaved) {
userWalletsListRepository.unlock(
userWallet.walletId,
unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse),
).onRight {
userWalletsListRepository.select(userWallet.walletId)
router.replaceAll(AppRoute.Wallet)
}
}
}
.onRight {
router.replaceAll(AppRoute.Wallet)
}

View file

@ -24,6 +24,7 @@ import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetContractAddressUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyPendingTracker
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory
@ -59,6 +60,7 @@ internal class YieldSupplyApproveModel @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val yieldSupplyGetContractAddressUseCase: YieldSupplyGetContractAddressUseCase,
private val yieldSupplyPendingTracker: YieldSupplyPendingTracker,
private val yieldSupplyAlertFactory: YieldSupplyAlertFactory,
) : Model(), YieldSupplyNotificationsComponent.ModelCallback {
@ -159,7 +161,12 @@ internal class YieldSupplyApproveModel @Inject constructor(
)
params.callback.onTransactionProgress(false)
},
ifRight = {
ifRight = { txHash ->
yieldSupplyPendingTracker.addPending(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,
txIds = listOf(txHash),
)
val event = AnalyticsParam.TxSentFrom.Earning(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,

View file

@ -5,9 +5,9 @@
# https://github.com/tangem/tangem-sdk-android/
# https://github.com/tangem/vico
tangemBlockchainSdk = "develop-1391"
tangemBlockchainSdk = "develop-1408"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-573"
tangemCardSdk = "develop-577"
#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 ^

@ -1 +1 @@
Subproject commit dc9df17919126bce93482559fe26602f9801eb42
Subproject commit 35755ad66f5f8fa6bcb1c84b443826cd315495d0