diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 8285f8d289..b55ca9ec9b 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 8285f8d2896d2994337f55a7b3a54fd5acc24304 +Subproject commit b55ca9ec9bc7ff88a8764f00656ee7a884df9125 diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index ff1b6bb97a..55dab8c3b9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -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, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 1b3b29d84b..6e58009dd5 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index 80dce033a7..414ea8a0b3 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -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()) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index cbc3285d4f..36257bc667 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -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, ) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 2be124d092..4b9bd5496d 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -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() diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt index 150bc43608..6bb24f24e3 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt @@ -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 { diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt index 43f01a3e93..a6a7a2a078 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt @@ -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, 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 diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt b/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt new file mode 100644 index 0000000000..a163c8ee15 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt @@ -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, 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, +): 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, 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 } \ No newline at end of file diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 837e466433..8fead6f8ae 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -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") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromoBannerV2Response.kt b/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromoBannerV2Response.kt new file mode 100644 index 0000000000..23b62757cb --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromoBannerV2Response.kt @@ -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, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 9d61d0b413..7dad440994 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -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 + + @GET("/v2/promotion") + suspend fun getPromoBannersV2( + @Query("walletId") walletId: String, + @Header("Cache-Control") cacheControl: String = "max-age=600", + ): ApiResponse // endregion /** diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 2a9bd1b3eb..4175f06e1c 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -899,6 +899,9 @@ Krypto mit SEPA kaufen Trage dich in die Warteliste ein und erhalte eine Zahlungskarte, die es so noch nie gab. Tangem Visa Card + Bedingungen + Zahlen Sie mindestens $100 ein, halten Sie den Betrag 30 Tage und erhalten Sie $10. + Yield-Mode-Kampagne Du musst einen einzigen Zugangscode einrichten, um alle deine Geräte zu schützen Schützen Du kannst später auf jeder Karte oder Ring einen individuellen Zugangscode einrichten diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 901dbaefc1..c3b8bb8a6a 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -299,6 +299,7 @@ Ir al token Entendido Ocultar + Mantener para %s hora Importe En progreso @@ -323,6 +324,7 @@ No Ninguna dirección No agregadas + No disponible Ahora no Ahora OK @@ -373,6 +375,7 @@ Intercambiar Tangem Tangem Wallet + Toque y mantenga presionado términos y condiciones Condiciones de uso A @@ -462,6 +465,11 @@ Enviar activos a otras redes resultará en una pérdida permanente. Red de %s Envíe fondos utilizando solo + Las mejores oportunidades + Todas las redes + Todos los tipos + Mayormente usado + Ganar Hola equipo de soporte, he encontrado un error con el código: %s Error de WalletConnect Ha usado una tarjeta de otra billetera. Toque la tarjeta asociada con esta billetera @@ -561,6 +569,8 @@ Comentarios en Tangem No se puede completar una transacción Error de descripción de moneda + Fondos insuficientes + Tarifa de transacción Ocurrió un error Ocurrió un error. Código: %s Requiere memo @@ -610,6 +620,7 @@ Finalizar la copia de seguridad primero Incompleto Otros métodos + 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. Guarde su frase de recuperación en un lugar seguro y manténgala en privado para proteger sus fondos. Frase de recuperación Para proteger su billetera con un código de acceso, complete el proceso de copia de seguridad. @@ -896,6 +907,9 @@ Comprar criptomonedas con SEPA Únete a la lista de espera y consigue una tarjeta de pago como ninguna otra. Tangem Visa Card + Términos y condiciones + Deposite $100 o más, manténgalo durante 30 días y reciba $10. + ¡Únase a la campaña Yield Mode! Configure un código de acceso único para proteger todos sus dispositivos. Proteger Puede configurar un código de acceso individual en cada tarjeta más adelante @@ -995,6 +1009,7 @@ Restaurar código de acceso Tarjetas idénticas Código de acceso + Solo puede tener una billetera móvil. Actualícela a la billetera de hardware Tangem o añada una nueva billetera de hardware. Todas las ofertas Disponible con %s La compra de criptomonedas en Tangem se lleva a cabo utilizando proveedores externos. @@ -1266,6 +1281,7 @@ La cantidad de staking se redondeará a %1$s TRX debido a las reglas de la red. La cantidad de unstaking se redondeará a %1$s TRX debido a las reglas de la red. APR %1$s%% + 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. Reclamar lo que no está en staking Tarifa de staking de la cuenta 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. @@ -1276,6 +1292,7 @@ APR APY Las recompensas se acumulan automáticamente en su saldo de staking diariamente. + Las recompensas se acumulan en su saldo de staking. Fondos ganados: %s Disponible Tasa de recompensa promedio ¿Cómo funciona el staking? @@ -1420,6 +1437,7 @@ Sencillo e intuitivo, permite cambiar tokens con solo unos cuantos toques Simplemente cómodo Intercambio a través del proveedor + Sus activos 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. 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 El importe incluye los honorarios del proveedor de servicios. @@ -1434,6 +1452,7 @@ Fondos insuficientes Dar autorización Intercambiar + Intercambiando... Usted recibe Elige token no disponible @@ -1452,6 +1471,9 @@ Mantén tu dinero seguro. Puedes desbloquear en cualquier momento. Tu tarjeta está congelada. Obtener ayuda + Razón: %s + %s · %s + MCC %s Otro No se puede usar en dispositivos rooteados Completado @@ -1528,10 +1550,15 @@ ¿Seguro que desea detener el proceso KYC? Puede retomarlo en cualquier momento. No pudimos verificar tu perfil. Si tienes alguna pregunta, contacta con el soporte. Lamentablemente, no pudimos verificar tu identidad + El proceso KYC ha fallado KYC en curso Ver estado KYC en progreso para Tangem Pay 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. + KYC rechazado + Ocultar el bloque KYC + Lo sentimos, no pudimos verificarle + su perfil. Obtén tu tarjeta virtual Tangem Visa gratuita Usa USDC para pagos cotidianos Obtener tarjeta @@ -1818,7 +1845,17 @@ Entendido ¡Realmente genial! Actualizar + Iniciar migración + Copiar + La firma de mensajes no es compatible con esta red + No se puede firmar el mensaje. Por favor, inténtelo de nuevo. 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. + Mensaje + Abrir el portal de reclamaciones + Para mantener el acceso a sus fondos, comience la migración de acuerdo con las pautas oficiales de Clore. + Migración de la red Clore + Firmar + Firma Migración de la red Clore Actualmente estás en el modo Demo Modo demo activo diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index f0253cda22..d88cdd4e57 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -43,6 +43,7 @@ Ajouter un compte Enregistrer Nom du compte + Un compte portant ce nom existe déjà. Veuillez choisir un autre nom. Nom de compte déjà utilisé Compte Nouveau compte @@ -298,6 +299,7 @@ Aller au jeton Compris Cacher + Maintenir contre %s heure Importez En cours @@ -322,6 +324,7 @@ Non Aucune adresse Non ajouté + Indisponible Pas maintenant Maintenant OK @@ -372,6 +375,7 @@ Échanger Tangem Tangem Wallet + Appuyez et maintenez enfoncé termes et conditions Conditions d\'utilisation À @@ -399,6 +403,8 @@ Mode Rendement Adresse du contrat copiée ! Réseaux disponibles + La dérivation de votre token correspond à la dérivation de %1$s. Votre token sera ajouté à ce compte. + La dérivation appartient à un autre compte. Jeton ajouté au compte %1$s Adresse du contrat L\'adresse du contrat n\'est pas valide @@ -459,6 +465,11 @@ L’envoi d’actifs sur d’autres réseaux entraînera une perte définitive. %s réseau Envoyez des fonds en utilisant uniquement + Meilleures opportunités + Tous les réseaux + Tous les types + Principalement utilisé + Gagner Bonjour équipe de support, j’ai rencontré une erreur avec le code : %s Erreur WalletConnect Vous avez utilisé une carte d\'un autre portefeuille. Appuyez sur la carte associée à ce portefeuille @@ -558,6 +569,8 @@ Commentaires sur Tangem Impossible d\'effectuer une transaction Erreur de description de la pièce + Fonds insuffisants + Frais de transaction Une erreur s\'est produite Une erreur s\'est produite. Code:%s Un mémo est requis @@ -607,6 +620,7 @@ Finalisez d\'abord la sauvegarde. Incomplet Autres méthodes + 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é. Conservez votre seed phrase dans un endroit sûr et gardez-la confidentielle afin de protéger vos fonds. Seed phrase Pour sécuriser votre wallet avec un code d\'accès, effectuez la procédure de sauvegarde. @@ -644,7 +658,7 @@ Une erreur s\'est produite pendant l\'opération. Vos fonds restent en sécurité et entièrement accessibles pendant toute la durée du processus. Accès aux fonds - 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. + 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. Sécurité générale Les clés privées seront transférées de l\'application vers votre hardware wallet Tangem. Migration des clés @@ -893,6 +907,9 @@ Acheter des cryptomonnaies avec SEPA Rejoignez la liste d’attente et obtenez une carte de paiement pas comme les autres. Tangem Visa Card + Conditions générales + Déposez 100 $ ou plus, conservez le dépôt pendant 30 jours et recevez 10 $. + Rejoignez la campagne Yield Mode ! Vous devez définir un seul code d\'accès pour protéger tous vos appareils. Protéger Vous pourrez définir un code d\'accès individuel sur chaque carte plus tard @@ -992,6 +1009,7 @@ Restauration du code d\'accès Cartes identiques Code d\'accès + Vous ne pouvez avoir qu\'un seul portefeuille mobile. Passez au portefeuille matériel Tangem ou ajoutez un nouveau portefeuille matériel. Toutes les offres Disponible avec %s L\'achat de crypto-monnaies sur Tangem est assuré par des fournisseurs tiers selon leurs conditions. @@ -1036,6 +1054,7 @@ Disponible auprès de Disponible jusqu\'à Vous obtenez + Le service est fourni par un prestataire externe. Tangem n\'est pas responsable. Vous pouvez vérifier l\'état de la transaction à partir de la page du jeton Jusqu\'à Via @@ -1248,18 +1267,21 @@ Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau. Facile à utiliser 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. - Pas de seedphrase + Pas de seed phrase Le meilleur hardware wallet Tangem Hardware Wallet Nom Faites travailler votre jeton pour vous + Les frais de réseau sont une petite somme à payer pour traiter et confirmer ta transaction sur la blockchain. 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. + Activation du compte Les frais de réseau ont changé. Veuillez vérifier le nouveau montant avant de continuer. Frais de réseau mis à jour Le montant à staker doit être au moins %s Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau. Le montant d\'annulation du staking sera arrondi à %1$s TRX en raison des règles du réseau. TAEG %1$s%% + 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. Réclamation déstakée Frais de staking du compte 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é. @@ -1270,6 +1292,7 @@ APR APY Les récompenses s\'accumulent automatiquement sur votre solde de staking quotidiennement. + Les récompenses sont ajoutées à votre solde de staking. Fonds gagnés : %s Disponible Taux de récompense moyen Qu\'est-ce que le Staking ? @@ -1325,6 +1348,7 @@ L\'option de restaker vous permet de déplacer vos fonds d\'un validateur à un autre sans avoir besoin de les déstaker. 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. Pour commencer le staking, vous devez d’abord activer votre compte TON + Activation du compte Pour commencer à staker TON, effectuez d\'abord une transaction sortante de n\'importe quel montant — cela activera votre portefeuille. Jusqu\'à 0,2 TON peuvent être requis en plus des frais de réseau pour terminer la transaction. Tout montant non utilisé sera remboursé. 0,2 TON requis pour effectuer cette opération, en plus des frais de réseau. Veuillez recharger votre solde. @@ -1413,6 +1437,7 @@ Simple et intuitif, vous permettant d\'échanger des jetons en quelques clics Plus Simple Que Jamais Échange via le fournisseur + Vos actifs 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. 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 Le montant comprend les frais du fournisseur de services. @@ -1427,6 +1452,7 @@ Fonds insuffisants Donner l\'autorisation Échanger + Échange... Vous recevez Choisir le jeton non disponible @@ -1445,6 +1471,9 @@ Geler Votre carte est gelée. Obtenir de l\'aide + Raison: %s + %s · %s + MCC %s Autre Impossible à utiliser sur les appareils rootés Terminé @@ -1521,10 +1550,15 @@ Êtes-vous sûr de vouloir interrompre le processus KYC ? Vous pouvez y revenir à tout moment. Nous n\'avons pas pu vérifier votre profil. Pour toute question, veuillez contacter le support. Malheureusement, nous n\'avons pas pu vérifier votre identité + KYC a échoué KYC en cours Voir le statut KYC en cours pour Tangem Pay 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. + KYC rejeté + Masquer le bloc KYC + Désolé, nous n\'avons pas pu vérifier. + votre profil. Obtenez votre carte virtuelle Tangem Visa gratuite Utilisez USDC pour les paiements quotidiens Obtenir la carte @@ -1579,6 +1613,7 @@ Jeton dans le %%image%% %1$s réseau 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 Impossible de masquer %s + Afficher le code QR Échangez ce jeton contre un autre moyennant des frais de service de %1$s du %2$s au %3$s février. Échangez avec Changelly, %s frais Échangez maintenant @@ -1710,6 +1745,12 @@ Scannez votre carte pour déverrouiller l\'accès Déverrouillage nécessaire Choisissez comment ajouter votre portefeuille + Scannez votre carte ou votre bague Tangem pour la restaurer ou l\'importer depuis un autre portefeuille. + Créer un Hardware Wallet + Vous souhaitez acheter un portefeuille Tangem ? + Importer la seed phrase + Restaurez votre portefeuille sur votre téléphone ou importez-le depuis une autre application — pratique, mais moins sécurisé qu\'une carte Tangem. + Que choisir ? La blockchain n\'est pas accessible. Réessayez plus tard Scanner la carte ou la bague 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é. @@ -1766,9 +1807,9 @@ Ajouter un portefeuille existant Dispositifs physiques qui stockent votre clé privée hors ligne en toute sécurité. Scanner un Tangem Wallet - Importez un portefeuille existant à l\'aide de votre seedphrase. + Importez un portefeuille existant à l\'aide de votre seed phrase. Importer un wallet - Entrez la seedphrase + Entrez la seed phrase Vous avez importé votre portefeuille avec succès. Importer un wallet Importation terminée @@ -1984,7 +2025,7 @@ Livraison rapide Commencez en un seul clic Sans faille et sécurisé - Pas de seedphrase + Pas de seed phrase Facile à utiliser Créez un harware wallet avec Tangem. Aussi fin qu\'une carte bancaire, aussi sûr qu\'un coffre-fort. Créer ou importer un portefeuille logiciel @@ -2105,4 +2146,5 @@ Impossible de couvrir les frais %s Le mode rendement n\'est pas disponible pour le moment. Veuillez réessayer plus tard. Le mode Rendement est indisponible + Impossible de charger le graphique... diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index cae90804ea..770eb98276 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -294,6 +294,7 @@ トークンへ移動 わかりました 非表示 + %sまで長押し 時間 インポート 進行中 @@ -368,6 +369,7 @@ スワップ Tangem Tangem Wallet + タップして長押し 利用規約 利用規約 宛先 @@ -456,6 +458,11 @@ 他のネットワークで資産を送金すると、永久に失われます。 %sネットワーク 下記のみを使用して資金を送金する + おすすめ + すべてのネットワーク + すべての種類 + よく使われています + 運用 こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。 WalletConnectエラー 別のウォレットのカードまたはリングを使用しました。このウォレットにリンクしているカードまたはリングをタップしてください。 @@ -887,6 +894,9 @@ SEPAで暗号資産を購入 事前申し込み受付中。他にない特別なカードを、いち早く体験しよう。 Tangem Visaカード + 利用規約 + 100ドル以上を入金し、30日間保持すると、10ドルを受け取れます。 + Yield Mode キャンペーンに参加しよう! すべてのデバイスを保護するには、単一のアクセスコードを設定してください。 保護する 後で各カードおよびリングに個別のアクセスコードを設定できます。 @@ -935,11 +945,11 @@ 生体認証 シードフレーズについてもっと読む - これらの %d 単語を以下の順番通りに書き留め、安全かつ秘密の場所に保管してください。 + 以下に表示される%d個の単語を順番どおりに書き留め、安全で他人に知られない場所に保管してください。 あなたのシードフレーズ - %d 単語 + %d個の単語 ウォレットをインポートするには、下のフィールドにシードフレーズを入力してください。 シードフレーズを生成する @@ -1252,6 +1262,7 @@ ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。 ネットワークルールにより、ステーキング解除の量は%1$s TRX に切り上げられます。 APR %1$s%% + デリゲーションがネットワークに登録され、集計されるまでの間、ステーキング報酬は5エポック後(約25日後)から開始されます。 ステーキング解除分を請求する ステーキングアカウント手数料 ステーキングアカウントとは、ステーキングされたSOLが保管される特別なアカウントです。トークンをバリデーターに委任し、取引の検証に参加して報酬を受け取る際に作成されます。ステーキングアカウントの作成には少額の手数料がかかりますが、ステーキング完了後に返金されます。 @@ -1441,6 +1452,9 @@ 一時停止 カードが凍結されています サポートを受ける + 理由:%s + %s・%s + MCC %s その他 Root化された端末では使用できません 完了 @@ -1643,25 +1657,69 @@ ウォレット名の変更 すべてのロックを解除 %sですべてをロック解除 + AML認証済み + 利用可能 + ブロック中 + 負債 + 利用上限 + その他(OTPなし) + 単一の取引 + 合計 %d日間利用可能 + 利用可能残高は、保留中の取引、ブロック中の金額、デビット残高を考慮した、実際に利用できる資金を示します。 + %1$sまで利用可能 残高と限度額 + 支払いアカウントの情報が見つかりません。サポートにお問い合わせください。 + トークンの情報を読み込めません。 + 情報を読み込み中です。しばらくお待ちください。 + コストの管理、セキュリティの向上、リスク管理のために利用上限が設けられています。店舗でのカード決済では期間内に%1$s、サブスクリプションや支払いなどのその他の取引では%2$sまで利用できます。 アクセスコードは、支払いアカウントの管理および不正アクセスからの保護に使用されます。 アクセスコード + アカウント有効化 + 登録手続きを開始したウォレットを選択し、ブロックチェーン上でアカウントを作成するための取引に署名してください。 + 有効化をキャンセル 本当に終了してもよろしいですか?中断したところから後で続行できます。 長くはかかりません。アカウントを設定しています。 長くはかかりません。アクティベーションを完了しています。 準備完了です! + その他のウォレット 4桁のコードを設定します。 \nお支払いの際に使用されます。 + PINコード PINコードを作成する PINの認証に失敗しました。もう一度お試しいただくか、別のコードを使用してください。 無効な暗証番号:連続や繰り返しを避けてください + 準備完了です! + Tangemカードを用意し、タップして承認してください。 + Tangemウォレットを準備してください。 + \nサードパーティのWebサイト\nで接続手続きを完了し、その後Tangemアプリに戻ってください。 ウェブサイトに移動 + ウォレット接続 + ウォレットを選択 + 有効化を続行 アカウントの設定を続けましょう。 お帰りなさい! + 有効化を開始 手順に従ってアカウントを設定してください。 ようこそ! + ブロックチェーン上の金額 + 通貨コード + 日付 + エラーコード + 取引詳細 + 加盟店カテゴリーコード + 加盟店の所在地 + 加盟店の国コード + 加盟店名 + リクエストID + ステータス + 取引 + 取引金額 + トランザクションハッシュ + 取引リクエスト + 取引ステータス + 種類 この取引に異議を唱える ロック解除 カードをスキャンしてアクセスロックを解除する diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 45f190c03a..b0e42bdf47 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -925,6 +925,9 @@ Покупайте крипту через SEPA Присоединяйтесь к списку ожидания и получите уникальную платёжную карту, не похожую ни на одну другую. Tangem Visa Card + Условия программы + Внесите депозит от $100, удерживайте его 30 дней и получите бонус $10 + Подключите Режим доходности! Установите единый код доступа для защиты всех ваших карт или колец Защита Установите индивидуальный код доступа для каждой карты или кольца позже. @@ -1800,7 +1803,17 @@ Понятно! Очень круто! Обновить + Начать миграцию + Копировать + Подписание сообщений не поддерживается в этой сети + Невозможно подписать сообщение. Пожалуйста, попробуй позже. Согласно официальной документации Clore, все монеты, полученные до 21 декабря, будут мигрированы в токен Clore (ERC-20); монеты, полученные после этой даты, — нет. Решение для перевода находится в разработке — следите за обновлениями. + Сообщение + Открыть портал клейма + Чтобы сохранить доступ к своим средствам, начните миграцию в соответствии с официальной инструкцией Clore. + Миграция сети Clore + Подписать + Подпись Миграция сети Clore Вы находитесь в режиме демо Демо режим включен diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 61ee270e13..67360c1ce3 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -44,6 +44,7 @@ Зберегти Назва акаунту Акаунт з такою назвою вже існує. Будь ласка, виберіть іншу назву. + Обліковий запис з таким ім’ям уже існує Акаунт Новий акаунт Додати акаунт @@ -908,6 +909,9 @@ Купуйте криптовалюту через SEPA Приєднуйтесь до списку очікування та отримайте унікальну платіжну картку, якої ще не було. Tangem Visa Card + Умови та положення + Внесіть депозит від $100, утримуйте його 30 днів та отримайте $10. + Кампанія Yield Mode! Налаштуйте єдиний код доступу для захисту всіх ваших карток або кілець Захист Встановіть індивідуальний код доступу для кожної картки або кільця пізніше. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index f583c64401..ed8c132108 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -86,6 +86,8 @@ Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. Default Legacy + Try to reset biometrics on your device or contact support + Authentication error How to scan Request support Try again @@ -908,6 +910,9 @@ Buy Crypto with SEPA Join the waitlist and get a payment card unlike any other Tangem Visa Card + Terms and conditions + Deposit $100+, hold for 30 days, get $10 + Join the Yield Mode Campaign! Set up a single access code to protect all your devices. Protect Set an individual access code for each card or ring later. @@ -1843,7 +1848,18 @@ Ok, Got it! Really cool! Refresh + Start migration + Copy + To keep access to your funds, start the migration according to the official Clore guidelines. + Message signing is not supported for this network + Unable to sign message. Please try again. According to Clore’s 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. + Message + Open Claim portal + To continue using your Clore tokens, you must complete the token migration according to the information on the Claim Portal. + Clore Network Migration + Sign + Signature Clore Network Migration You are currently in the Demo mode Demo mode active diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index 631d330388..d76f2160fb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -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), ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index ddfbb831bf..4b016c5544 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -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, diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/SharedTransitionUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/SharedTransitionUtils.kt index 4851e2e34f..801c42088c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/SharedTransitionUtils.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/SharedTransitionUtils.kt @@ -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 { - error("No SharedTransitionScope provided") +private val LocalSharedTransitionScope = staticCompositionLocalOf { 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 + } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/bg_trending_article.webp b/core/ui/src/main/res/drawable/bg_trending_article.webp new file mode 100644 index 0000000000..42951498d8 Binary files /dev/null and b/core/ui/src/main/res/drawable/bg_trending_article.webp differ diff --git a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt index 10b55769a9..559ba4c6ce 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt @@ -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, ) } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt index 68bce0a025..b75e956b6e 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt @@ -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 { + return getAccountsResponseStore(userWalletId = userWalletId).data.filterNotNull() + } + override suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse? { return getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull() } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt b/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt index f03409232e..3973b17662 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt @@ -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 = 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.findAccount( @@ -232,4 +223,20 @@ internal class DefaultMainAccountTokensMigration( accountNodeValue == derivationIndex.value.toLong() } } + + private suspend fun Raise.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) + }, + ) + } } \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt index 49586e40b0..1dfc937810 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt @@ -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 { diff --git a/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt b/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt index 150d3ec917..70d14974b1 100644 --- a/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/token/DefaultMainAccountTokensMigrationTest.kt @@ -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(value = null) private val accountTokenMigrationStore = mockk(relaxUnitFun = true) private val userTokensSaver = mockk(relaxUnitFun = true) + private val eTagsStore = mockk(relaxUnitFun = true) private val migration = DefaultMainAccountTokensMigration( accountsResponseStoreFactory = accountsResponseStoreFactory, accountTokenMigrationStore = accountTokenMigrationStore, userTokensSaver = userTokensSaver, + eTagsStore = eTagsStore, + dispatchers = TestingCoroutineDispatcherProvider(), ) private val userWalletId = UserWalletId("011") diff --git a/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsFetcher.kt b/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsFetcher.kt index 4004ed3fcf..81d7900564 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsFetcher.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/account/WalletAccountsFetcher.kt @@ -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 + /** Get saved wallet accounts by [userWalletId] */ suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse? } \ No newline at end of file diff --git a/data/onramp/build.gradle.kts b/data/onramp/build.gradle.kts index ada7a8f3b0..bf0adddbac 100644 --- a/data/onramp/build.gradle.kts +++ b/data/onramp/build.gradle.kts @@ -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) diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index b1552db7cc..5b985a43fc 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -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>> { 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() } } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index 2e80138833..a4e93fec4f 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -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, ) } diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index 31133bb96f..625fdc2535 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -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 } } \ No newline at end of file diff --git a/domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt b/domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt index 9f8055dc60..ee31586b29 100644 --- a/domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt +++ b/domain/promo/models/src/main/java/com/tangem/domain/promo/models/PromoBanner.kt @@ -31,4 +31,5 @@ enum class PromoId { VisaPresale, BlackFriday, OnePlusOne, + YieldPromo, } \ No newline at end of file diff --git a/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowPromoWalletUseCase.kt b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowPromoWalletUseCase.kt index 5cab5b1cde..7f6f41114d 100644 --- a/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowPromoWalletUseCase.kt +++ b/domain/promo/src/main/java/com/tangem/domain/promo/ShouldShowPromoWalletUseCase.kt @@ -33,6 +33,7 @@ class ShouldShowPromoWalletUseCase( PromoId.VisaPresale, PromoId.BlackFriday, PromoId.OnePlusOne, + PromoId.YieldPromo, -> true PromoId.Sepa -> { val walletFirstUsageDate = settingsRepository.getWalletFirstUsageDate() diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt index 3d3f0fbdb1..df7fde5ad2 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/PromoAnalyticsEvent.kt @@ -60,5 +60,6 @@ sealed class PromoAnalyticsEvent( Sepa("Sepa"), BlackFriday("Black Friday"), OnePlusOne("One-Plus-One"), + YieldPromo("Yield Promo"), } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/details/NavigationAction.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/details/NavigationAction.kt index e3343ec90b..ca8dbfd9f9 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/details/NavigationAction.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/details/NavigationAction.kt @@ -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() } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SignCloreMessageError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SignCloreMessageError.kt new file mode 100644 index 0000000000..a87418223c --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SignCloreMessageError.kt @@ -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() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignCloreMessageUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignCloreMessageUseCase.kt new file mode 100644 index 0000000000..c4e4913822 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignCloreMessageUseCase.kt @@ -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 { + 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() + } + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt index 703dcb8cbb..7f8f5b9916 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/Settings.kt @@ -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 + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt index 1c23c85475..be4e913124 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt @@ -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"), diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index 77f03f306a..6460bfbfd7 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -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 { + suspend operator fun invoke( + userWallet: UserWallet, + canOverride: Boolean = false, + analyticsSource: AnalyticsParam.ScreensSources? = null, + ): Either { 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)) + } + } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index e56710ead0..86ecb2a42f 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -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 = {}, ), ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt index bc4baf7d42..a8ef5eb141 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -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, val isWalletSavingInProgress: Boolean, val addNewWalletText: TextReference, - @DrawableRes val addNewWalletIconRes: Int?, val onAddNewWalletClick: () -> Unit, ) \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 78426bd036..fbf60da951 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -55,7 +55,6 @@ internal class UserWalletListModel @Inject constructor( isWalletSavingInProgress = false, addNewWalletText = TextReference.EMPTY, onAddNewWalletClick = ::onAddNewWalletClick, - addNewWalletIconRes = R.drawable.ic_plus_24, ), ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index f06856c4df..e253ffec97 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -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, ) } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt index 7133bc0d73..1db2f97cc0 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -73,7 +73,12 @@ internal class UserWalletSaver @Inject constructor( private suspend fun Raise.saveWallet(userWallet: UserWallet) { fold( - block = { saveWalletUseCase(userWallet).bind() }, + block = { + saveWalletUseCase( + userWallet = userWallet, + analyticsSource = AnalyticsParam.ScreensSources.Settings, + ).bind() + }, recover = { error -> when (error) { is SaveWalletError.WalletAlreadySaved -> { diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt index 9d221287dd..b868b9b8be 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt @@ -12,8 +12,7 @@ import kotlinx.serialization.Serializable data class AvailableToAddData( val availableToAddWallets: Map, ) { - 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, val availableNetworks: Set, val availableToAddAccounts: Map, -) +) { + 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 = availableNetworks .filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } } .toSet() diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt index 49d3820aa2..a6b02044fb 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt @@ -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, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt index b31e57f8b0..a06d46878d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -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, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt index d6e5ec6be2..dc78f2b7a0 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt @@ -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() { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index 9b5b58f8d5..3cb3671be5 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -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) { diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index 099a3e49c3..cc3d07c342 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -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) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt index 39a2c8b242..5d32c8952b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt @@ -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) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt index 3ca633edf5..976ad0a6c4 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt @@ -12,8 +12,7 @@ import kotlinx.serialization.Serializable internal data class AvailableToAddData( val availableToAddWallets: Map, ) { - 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, val availableNetworks: Set, val availableToAddAccounts: Map, -) +) { + 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, val addedNetworks: Set, ) { + 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 = availableNetworks .filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } } .toSet() diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt index a9a11d6eb7..3a311b6854 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt @@ -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, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt index b0198b5975..6b0fd6a169 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -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, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt index 8d508c9a4b..32c406b3b1 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt @@ -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) + } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt index 1cb37f4eb6..42d2db0372 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt @@ -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) + } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 159ccf4bd9..0fb3c2f011 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -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 } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/upgradewallet/model/MultiWalletUpgradeWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/upgradewallet/model/MultiWalletUpgradeWalletModel.kt index f6b6306cd6..d7e2c6e3e9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/upgradewallet/model/MultiWalletUpgradeWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/upgradewallet/model/MultiWalletUpgradeWalletModel.kt @@ -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) + } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt index 51db083121..b286fead4f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt @@ -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 { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index e70843bd7b..79c8741734 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -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) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index 235c201961..abc373283f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -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) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index fe05161fec..9bc9fb3314 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -70,7 +70,6 @@ internal object SwapAmountQuoteUtils { } return copy( - isPrimaryButtonEnabled = updatedAmountField.amountField.isPrimaryButtonEnabled, primaryAmount = updatedAmountField, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 5d53b1d40e..579a4ddafc 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -179,7 +179,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( val swapDirection: SwapDirection, val isBalanceHidingFlow: StateFlow, val primaryCryptoCurrencyStatusFlow: StateFlow, - val primaryFeePaidCurrencyStatusFlow: StateFlow, + val primaryFeePaidCurrencyStatusFlow: StateFlow, // doesn't change if select gasless fee val accountFlow: StateFlow, val isAccountModeFlow: StateFlow, val callback: ModelCallback, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 66d822dd46..97a3d13b93 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -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( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 4c063c8564..a8b55e5af2 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -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(), ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index 55e17a1292..1ab3e27465 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -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()) { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 6ae4815817..fd342433db 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -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>() - 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, updateFeeBlock: Boolean = true, ): PeriodicTask> { + 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.Loading) + override val state = MutableStateFlow( + FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true), + ) override suspend fun loadFeeExtended( selectedToken: CryptoCurrencyStatus?, ): Either { - 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 { - 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) diff --git a/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt index df83da98da..e8c5c6d08d 100644 --- a/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt +++ b/features/tangempay/onboarding/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayOnboardingComponent.kt @@ -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() } diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index a3dc4ea532..b8ce2a6fac 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -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, -> { diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 05ec40ad40..10cb869ad1 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -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) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/CloreMigrationModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/CloreMigrationModel.kt new file mode 100644 index 0000000000..51e1125b6b --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/CloreMigrationModel.kt @@ -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 = "", +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt index bf41cabfe1..bc6d1d4b7e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt @@ -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 } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index bd23fecfce..a137a93bf5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -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 = 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 = 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 } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 5733db0e18..feb6279306 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -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( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index eb54aca878..38588b2f69 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -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 } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 161f4ddb5e..323b323e72 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -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 } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index b85754cda3..abf30bd8f0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -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) + } } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheet.kt new file mode 100644 index 0000000000..9875449e80 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheet.kt @@ -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( + 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 \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheetConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheetConfig.kt new file mode 100644 index 0000000000..9adf70e4af --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/clore/CloreMigrationBottomSheetConfig.kt @@ -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 \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index 57040c28d4..1318303b1a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -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)) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index 05ca37c44e..8ad405bb28 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -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) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index b4a257bd42..a439494235 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -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) @@ -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" } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt index af455b27ac..0cb63b07c9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt @@ -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) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 08a1123ea0..2996d749be 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -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(), ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index 1eb6944175..e766bf4333 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -72,7 +72,7 @@ sealed class WalletScreenAnalyticsEvent { } put("Wallet Type", seedPhrase) }, - ) + ), AppsFlyerIncludedEvent data class NoticeFinishActivation( private val activationState: ActivationState, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 94ede58c14..3852ada00e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -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) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index b757338349..fe3861e0c2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -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.addYieldPromoNotification( + clickIntents: WalletClickIntents, + shouldShowPromo: Boolean, + ) { + addIf( + element = WalletNotification.YieldPromo( + onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.YieldPromo) }, + onTermsAndConditionsClick = { clickIntents.onYieldPromoTermsAndConditionsClick() }, + ), + condition = shouldShowPromo, + ) + } + // private fun MutableList.addYieldSupplyNotifications( // flattenCurrencies: Lce>, // ) { @@ -317,6 +336,29 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( element = WalletNotification.Warning.SomeNetworksUnreachable, condition = flattenCurrencies.hasUnreachableNetworks(), ) + + addCloreMigrationNotification(flattenCurrencies, clickIntents) + } + + private fun MutableList.addCloreMigrationNotification( + flattenCurrencies: Lce>, + clickIntents: WalletClickIntents, + ) { + val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return + + add( + WalletNotification.CloreMigration( + onStartMigrationClick = { clickIntents.onCloreMigrationClick(cloreCurrency) }, + ), + ) + } + + private fun Lce>.findCloreCurrency(): CryptoCurrencyStatus? { + val currencies = getOrNull(isPartialContentAccepted = true) ?: return null + + return currencies.find { currencyStatus -> + BlockchainUtils.isClore(currencyStatus.currency.network.rawId) + } } private fun MutableList.addPushReminderNotification( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index e2dc6648fc..9abf719a45 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -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, + ), + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt index 66c94df727..1ddf7dd2cf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt @@ -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, ), ) { diff --git a/features/wallet/impl/src/main/res/drawable/ic_yield_promo_36.xml b/features/wallet/impl/src/main/res/drawable/ic_yield_promo_36.xml new file mode 100644 index 0000000000..4a4921466d --- /dev/null +++ b/features/wallet/impl/src/main/res/drawable/ic_yield_promo_36.xml @@ -0,0 +1,15 @@ + + + + diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index 5712dbb7ff..6d0b116353 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -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) } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt index ca94e2df0c..4ac6a9f1ce 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt @@ -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, diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 81757bf804..1a6ce6fac6 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -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 ^ diff --git a/tangem-android-tools b/tangem-android-tools index dc9df17919..35755ad66f 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit dc9df17919126bce93482559fe26602f9801eb42 +Subproject commit 35755ad66f5f8fa6bcb1c84b443826cd315495d0