Updated on 2026-08-14
This commit is contained in:
commit
f3fdcb3e3f
97 changed files with 1627 additions and 324 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 8285f8d2896d2994337f55a7b3a54fd5acc24304
|
||||
Subproject commit b55ca9ec9bc7ff88a8764f00656ee7a884df9125
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ package com.tangem.common.ui.news
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -11,6 +14,8 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
|
@ -21,15 +26,12 @@ import com.tangem.core.ui.components.SpacerH
|
|||
import com.tangem.core.ui.components.SpacerHMax
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.label.Label
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.utils.StringsSigns
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
|
@ -65,52 +67,61 @@ private fun TrendingArticle(
|
|||
colors: CardColors,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(colors.containerColor)
|
||||
.clickable { onArticleClick() }
|
||||
.padding(vertical = 24.dp, horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
.clickable { onArticleClick() },
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
text = stringResourceSafe(R.string.feed_trending_now),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
Image(
|
||||
modifier = Modifier.matchParentSize(),
|
||||
painter = painterResource(R.drawable.bg_trending_article),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
text = stringResourceSafe(R.string.feed_trending_now),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
SpacerH(12.dp)
|
||||
|
||||
Text(
|
||||
text = articleConfigUM.title,
|
||||
color = if (articleConfigUM.isViewed) {
|
||||
TangemTheme.colors.text.tertiary
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
style = TangemTheme.typography.h3,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
text = articleConfigUM.title,
|
||||
color = if (articleConfigUM.isViewed) {
|
||||
TangemTheme.colors.text.tertiary
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
style = TangemTheme.typography.h3,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
SpacerH(8.dp)
|
||||
SpacerH(8.dp)
|
||||
|
||||
ArticleInfo(
|
||||
score = articleConfigUM.score,
|
||||
createdAt = articleConfigUM.createdAt.resolveReference(),
|
||||
)
|
||||
ArticleInfo(
|
||||
score = articleConfigUM.score,
|
||||
createdAt = articleConfigUM.createdAt.resolveReference(),
|
||||
)
|
||||
|
||||
SpacerH(32.dp)
|
||||
SpacerH(32.dp)
|
||||
|
||||
Tags(
|
||||
modifier = Modifier.padding(horizontal = 46.dp),
|
||||
tags = articleConfigUM.tags.toImmutableList(),
|
||||
)
|
||||
Tags(
|
||||
modifier = Modifier.padding(horizontal = 46.dp),
|
||||
tags = articleConfigUM.tags.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,46 +193,14 @@ private fun DefaultArticle(
|
|||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
SpacerHMax(modifier = Modifier.padding(bottom = 16.dp))
|
||||
SpacerH(16.dp)
|
||||
|
||||
SpacerHMax()
|
||||
|
||||
Tags(tags = articleConfigUM.tags.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
|
||||
FlowRow(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
maxItemsInEachRow = MAX_TAGS_COUNT_TO_SHOW + 1,
|
||||
maxLines = 1,
|
||||
itemVerticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
val displayedTags = if (tags.size > MAX_TAGS_COUNT_TO_SHOW) {
|
||||
tags.take(MAX_TAGS_COUNT_TO_SHOW)
|
||||
} else {
|
||||
tags
|
||||
}
|
||||
|
||||
displayedTags.forEach { tag ->
|
||||
Label(state = tag)
|
||||
}
|
||||
|
||||
if (tags.size > MAX_TAGS_COUNT_TO_SHOW) {
|
||||
val remainingItems = tags.size - MAX_TAGS_COUNT_TO_SHOW
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("${StringsSigns.PLUS}$remainingItems"),
|
||||
maxLines = 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val MAX_TAGS_COUNT_TO_SHOW = 3
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
|
|
|
|||
128
common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt
Normal file
128
common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
package com.tangem.common.ui.news
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Placeable
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.layout.SubcomposeMeasureScope
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.label.Label
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.utils.StringsSigns
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
private val TAG_SPACING = 4.dp
|
||||
|
||||
/**
|
||||
* A layout that displays a list of tags, automatically calculating how many can fit
|
||||
* in a single line. If not all tags fit, it shows an overflow indicator (e.g., "+N").
|
||||
*
|
||||
* @param tags The immutable list of [LabelUM] to display.
|
||||
* @param modifier The modifier to be applied to the layout.
|
||||
*/
|
||||
@Composable
|
||||
internal fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
|
||||
if (tags.isEmpty()) return
|
||||
|
||||
SubcomposeLayout(modifier = modifier) { constraints ->
|
||||
val spacingPx = TAG_SPACING.roundToPx()
|
||||
|
||||
val tagPlaceables = subcompose(ContentSlot.Tags) {
|
||||
tags.forEach { tag ->
|
||||
Label(state = tag)
|
||||
}
|
||||
}.map { it.measure(constraints) }
|
||||
|
||||
val layoutInfo = calculateLayoutInfo(
|
||||
maxWidth = constraints.maxWidth,
|
||||
spacingPx = spacingPx,
|
||||
tagPlaceables = tagPlaceables,
|
||||
)
|
||||
|
||||
val overflowPlaceable = layoutInfo.overflowCount.takeIf { it > 0 }?.let {
|
||||
subcompose(ContentSlot.Overflow) {
|
||||
OverflowLabel(count = it)
|
||||
}.first().measure(constraints)
|
||||
}
|
||||
|
||||
val visiblePlaceables = tagPlaceables.take(layoutInfo.visibleCount)
|
||||
val allVisibleItems = visiblePlaceables + listOfNotNull(overflowPlaceable)
|
||||
|
||||
val width = calculateRowWidth(
|
||||
placeables = allVisibleItems,
|
||||
spacingPx = spacingPx,
|
||||
).coerceIn(constraints.minWidth, constraints.maxWidth)
|
||||
|
||||
val height = allVisibleItems.maxOfOrNull { it.height } ?: 0
|
||||
|
||||
layout(width = width, height = height) {
|
||||
var xPosition = 0
|
||||
allVisibleItems.forEachIndexed { index, placeable ->
|
||||
if (index > 0) {
|
||||
xPosition += spacingPx
|
||||
}
|
||||
val yPosition = height - placeable.height
|
||||
placeable.placeRelative(xPosition, yPosition)
|
||||
xPosition += placeable.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates how many tags can be displayed and how many are overflowing.
|
||||
*/
|
||||
private fun SubcomposeMeasureScope.calculateLayoutInfo(
|
||||
maxWidth: Int,
|
||||
spacingPx: Int,
|
||||
tagPlaceables: List<Placeable>,
|
||||
): TagsLayoutInfo {
|
||||
val totalWidthWithoutOverflow = calculateRowWidth(tagPlaceables, spacingPx)
|
||||
|
||||
if (totalWidthWithoutOverflow <= maxWidth) {
|
||||
return TagsLayoutInfo(visibleCount = tagPlaceables.size, overflowCount = 0)
|
||||
}
|
||||
|
||||
for (count in tagPlaceables.indices.reversed()) {
|
||||
val visibleTags = tagPlaceables.take(count)
|
||||
val overflowCount = tagPlaceables.size - count
|
||||
|
||||
val tempOverflowPlaceable = subcompose("overflow_temp_$overflowCount") {
|
||||
OverflowLabel(count = overflowCount)
|
||||
}.first().measure(Constraints())
|
||||
|
||||
val widthWithOverflow = calculateRowWidth(
|
||||
placeables = visibleTags + tempOverflowPlaceable,
|
||||
spacingPx = spacingPx,
|
||||
)
|
||||
|
||||
if (widthWithOverflow <= maxWidth) {
|
||||
return TagsLayoutInfo(visibleCount = count, overflowCount = overflowCount)
|
||||
}
|
||||
}
|
||||
|
||||
return TagsLayoutInfo(visibleCount = 0, overflowCount = tagPlaceables.size)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OverflowLabel(count: Int) {
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("${StringsSigns.PLUS}$count"),
|
||||
maxLines = 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun calculateRowWidth(placeables: List<Placeable>, spacingPx: Int): Int {
|
||||
if (placeables.isEmpty()) return 0
|
||||
val tagsWidth = placeables.sumOf { it.width }
|
||||
val spacingWidth = spacingPx * (placeables.size - 1).coerceAtLeast(0)
|
||||
return tagsWidth + spacingWidth
|
||||
}
|
||||
|
||||
private data class TagsLayoutInfo(val visibleCount: Int, val overflowCount: Int)
|
||||
|
||||
private enum class ContentSlot { Tags, Overflow }
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.promotion.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PromoBannerV2Response(
|
||||
@Json(name = "promotions")
|
||||
val promotions: List<PromoBannerResponse>,
|
||||
)
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.api.tangemTech
|
|||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.promotion.models.PromoBannerResponse
|
||||
import com.tangem.datasource.api.promotion.models.PromoBannerV2Response
|
||||
import com.tangem.datasource.api.promotion.models.StoryContentResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.*
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
|
|
@ -188,6 +189,12 @@ interface TangemTechApi {
|
|||
@Query("programName") name: String,
|
||||
@Header("Cache-Control") cacheControl: String = "max-age=600",
|
||||
): ApiResponse<PromoBannerResponse>
|
||||
|
||||
@GET("/v2/promotion")
|
||||
suspend fun getPromoBannersV2(
|
||||
@Query("walletId") walletId: String,
|
||||
@Header("Cache-Control") cacheControl: String = "max-age=600",
|
||||
): ApiResponse<PromoBannerV2Response>
|
||||
// endregion
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -899,6 +899,9 @@
|
|||
<string name="notification_sepa_title">Krypto mit SEPA kaufen</string>
|
||||
<string name="notification_visa_waitlist_promo_text">Trage dich in die Warteliste ein und erhalte eine Zahlungskarte, die es so noch nie gab.</string>
|
||||
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
|
||||
<string name="notification_yield_promo_button">Bedingungen</string>
|
||||
<string name="notification_yield_promo_text">Zahlen Sie mindestens $100 ein, halten Sie den Betrag 30 Tage und erhalten Sie $10.</string>
|
||||
<string name="notification_yield_promo_title">Yield-Mode-Kampagne</string>
|
||||
<string name="onboarding_access_code_feature_1_description">Du musst einen einzigen Zugangscode einrichten, um alle deine Geräte zu schützen</string>
|
||||
<string name="onboarding_access_code_feature_1_title">Schützen</string>
|
||||
<string name="onboarding_access_code_feature_2_description">Du kannst später auf jeder Karte oder Ring einen individuellen Zugangscode einrichten</string>
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@
|
|||
<string name="common_go_to_token">Ir al token</string>
|
||||
<string name="common_got_it">Entendido</string>
|
||||
<string name="common_hide">Ocultar</string>
|
||||
<string name="common_hold_to">Mantener para %s</string>
|
||||
<string name="common_hour">hora</string>
|
||||
<string name="common_import">Importe</string>
|
||||
<string name="common_in_progress">En progreso</string>
|
||||
|
|
@ -323,6 +324,7 @@
|
|||
<string name="common_no">No</string>
|
||||
<string name="common_no_address">Ninguna dirección</string>
|
||||
<string name="common_not_added">No agregadas</string>
|
||||
<string name="common_not_available">No disponible</string>
|
||||
<string name="common_not_now">Ahora no</string>
|
||||
<string name="common_now">Ahora</string>
|
||||
<string name="common_ok">OK</string>
|
||||
|
|
@ -373,6 +375,7 @@
|
|||
<string name="common_swap">Intercambiar</string>
|
||||
<string name="common_tangem">Tangem</string>
|
||||
<string name="common_tangem_wallet">Tangem Wallet</string>
|
||||
<string name="common_tap_and_hold_hint">Toque y mantenga presionado</string>
|
||||
<string name="common_terms_and_conditions">términos y condiciones</string>
|
||||
<string name="common_terms_of_use">Condiciones de uso</string>
|
||||
<string name="common_to">A</string>
|
||||
|
|
@ -462,6 +465,11 @@
|
|||
<string name="domain_receive_assets_onboarding_description">Enviar activos a otras redes resultará en una pérdida permanente.</string>
|
||||
<string name="domain_receive_assets_onboarding_network_name">Red de %s</string>
|
||||
<string name="domain_receive_assets_onboarding_title">Envíe fondos utilizando solo</string>
|
||||
<string name="earn_best_opportunities">Las mejores oportunidades</string>
|
||||
<string name="earn_filter_all_networks">Todas las redes</string>
|
||||
<string name="earn_filter_all_types">Todos los tipos</string>
|
||||
<string name="earn_mostly_used">Mayormente usado</string>
|
||||
<string name="earn_title">Ganar</string>
|
||||
<string name="email_preface_wc_error">Hola equipo de soporte, he encontrado un error con el código: %s</string>
|
||||
<string name="email_subject_wc_error">Error de WalletConnect</string>
|
||||
<string name="error_wrong_wallet_tapped">Ha usado una tarjeta de otra billetera. Toque la tarjeta asociada con esta billetera</string>
|
||||
|
|
@ -561,6 +569,8 @@
|
|||
<string name="feedback_subject_support_tangem">Comentarios en Tangem</string>
|
||||
<string name="feedback_subject_tx_failed">No se puede completar una transacción</string>
|
||||
<string name="feedback_token_description_error">Error de descripción de moneda</string>
|
||||
<string name="gasless_not_enough_funds_to_cover_token_fee">Fondos insuficientes</string>
|
||||
<string name="gasless_transaction_fee">Tarifa de transacción</string>
|
||||
<string name="generic_error">Ocurrió un error</string>
|
||||
<string name="generic_error_code">Ocurrió un error. Código: %s</string>
|
||||
<string name="generic_requires_memo_error">Requiere memo</string>
|
||||
|
|
@ -610,6 +620,7 @@
|
|||
<string name="hw_backup_need_title">Finalizar la copia de seguridad primero</string>
|
||||
<string name="hw_backup_no_backup">Incompleto</string>
|
||||
<string name="hw_backup_section_other_title">Otros métodos</string>
|
||||
<string name="hw_backup_seed_code_description">Guarde su frase de recuperación en un lugar seguro y manténgala privada para proteger sus fondos y configure un código de acceso para mayor seguridad.</string>
|
||||
<string name="hw_backup_seed_description">Guarde su frase de recuperación en un lugar seguro y manténgala en privado para proteger sus fondos.</string>
|
||||
<string name="hw_backup_seed_title">Frase de recuperación</string>
|
||||
<string name="hw_backup_to_secure_description">Para proteger su billetera con un código de acceso, complete el proceso de copia de seguridad.</string>
|
||||
|
|
@ -896,6 +907,9 @@
|
|||
<string name="notification_sepa_title">Comprar criptomonedas con SEPA</string>
|
||||
<string name="notification_visa_waitlist_promo_text">Únete a la lista de espera y consigue una tarjeta de pago como ninguna otra.</string>
|
||||
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
|
||||
<string name="notification_yield_promo_button">Términos y condiciones</string>
|
||||
<string name="notification_yield_promo_text">Deposite $100 o más, manténgalo durante 30 días y reciba $10.</string>
|
||||
<string name="notification_yield_promo_title">¡Únase a la campaña Yield Mode!</string>
|
||||
<string name="onboarding_access_code_feature_1_description">Configure un código de acceso único para proteger todos sus dispositivos.</string>
|
||||
<string name="onboarding_access_code_feature_1_title">Proteger</string>
|
||||
<string name="onboarding_access_code_feature_2_description">Puede configurar un código de acceso individual en cada tarjeta más adelante</string>
|
||||
|
|
@ -995,6 +1009,7 @@
|
|||
<string name="onboarding_wallet_info_title_fourth">Restaurar código de acceso</string>
|
||||
<string name="onboarding_wallet_info_title_second">Tarjetas idénticas</string>
|
||||
<string name="onboarding_wallet_info_title_third">Código de acceso</string>
|
||||
<string name="only_one_mobile_wallet_explanation">Solo puede tener una billetera móvil. Actualícela a la billetera de hardware Tangem o añada una nueva billetera de hardware.</string>
|
||||
<string name="onramp_all_offers_button_title">Todas las ofertas</string>
|
||||
<string name="onramp_avaiable_with_payment_methods">Disponible con %s</string>
|
||||
<string name="onramp_choose_provider_title_hint">La compra de criptomonedas en Tangem se lleva a cabo utilizando proveedores externos.</string>
|
||||
|
|
@ -1266,6 +1281,7 @@
|
|||
<string name="staking_amount_tron_integer_error">La cantidad de staking se redondeará a %1$s TRX debido a las reglas de la red.</string>
|
||||
<string name="staking_amount_tron_integer_error_unstaking">La cantidad de unstaking se redondeará a %1$s TRX debido a las reglas de la red.</string>
|
||||
<string name="staking_apr_earn_badge">APR %1$s%%</string>
|
||||
<string name="staking_cardano_details_rewards_info_text">Sus recompensas de staking comenzarán después de 5 épocas (~25 días) mientras su delegación esté registrada y contabilizada por la red.</string>
|
||||
<string name="staking_claim_unstaked">Reclamar lo que no está en staking</string>
|
||||
<string name="staking_details_account_fee">Tarifa de staking de la cuenta </string>
|
||||
<string name="staking_details_account_fee_info">Una cuenta de staking es una cuenta especial donde se almacenan los tokens SOL de staking. Se crea cuando delegas sus tokens a un validador para participar en la validación de transacciones y ganar recompensas. Se cobra una pequeña tarifa por crear la cuenta de staking, que se devuelve una vez que se completa el staking.</string>
|
||||
|
|
@ -1276,6 +1292,7 @@
|
|||
<string name="staking_details_apr">APR</string>
|
||||
<string name="staking_details_apy">APY</string>
|
||||
<string name="staking_details_auto_claiming_rewards_daily_text">Las recompensas se acumulan automáticamente en su saldo de staking diariamente.</string>
|
||||
<string name="staking_details_autocompound_rewards_earned">Las recompensas se acumulan en su saldo de staking. Fondos ganados: %s</string>
|
||||
<string name="staking_details_available">Disponible</string>
|
||||
<string name="staking_details_average_reward_rate">Tasa de recompensa promedio</string>
|
||||
<string name="staking_details_banner_text">¿Cómo funciona el staking?</string>
|
||||
|
|
@ -1420,6 +1437,7 @@
|
|||
<string name="swap_story_third_subtitle">Sencillo e intuitivo, permite cambiar tokens con solo unos cuantos toques</string>
|
||||
<string name="swap_story_third_title">Simplemente cómodo</string>
|
||||
<string name="swap_via_provider">Intercambio a través del proveedor</string>
|
||||
<string name="swap_your_assets_title">Sus activos</string>
|
||||
<string name="swapping_alert_cex_description">La cantidad incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario.</string>
|
||||
<string name="swapping_alert_cex_description_with_slippage">El importe incluye:\n- comisión del proveedor de servicios\n- comisión de red por el envío de %1$s desde el exchange de vuelta a la dirección del usuario. \n\nEl deslizamiento (slippage) del proveedor es de hasta %2$s</string>
|
||||
<string name="swapping_alert_dex_description">El importe incluye los honorarios del proveedor de servicios.</string>
|
||||
|
|
@ -1434,6 +1452,7 @@
|
|||
<string name="swapping_insufficient_funds">Fondos insuficientes</string>
|
||||
<string name="swapping_permission_header">Dar autorización</string>
|
||||
<string name="swapping_swap_action">Intercambiar</string>
|
||||
<string name="swapping_swap_action_in_progress">Intercambiando...</string>
|
||||
<string name="swapping_to_title">Usted recibe</string>
|
||||
<string name="swapping_token_list_title">Elige token</string>
|
||||
<string name="swapping_token_not_available">no disponible</string>
|
||||
|
|
@ -1452,6 +1471,9 @@
|
|||
<string name="tangem_pay_freeze_card_freeze">Mantén tu dinero seguro. Puedes desbloquear en cualquier momento.</string>
|
||||
<string name="tangem_pay_freeze_card_success">Tu tarjeta está congelada.</string>
|
||||
<string name="tangem_pay_get_help">Obtener ayuda</string>
|
||||
<string name="tangem_pay_history_item_spend_mc_declined_reason">Razón: %s</string>
|
||||
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s · %s</string>
|
||||
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
|
||||
<string name="tangem_pay_other">Otro</string>
|
||||
<string name="tangem_pay_rooted_device_subtitle">No se puede usar en dispositivos rooteados</string>
|
||||
<string name="tangem_pay_status_completed">Completado</string>
|
||||
|
|
@ -1528,10 +1550,15 @@
|
|||
<string name="tangempay_kyc_confirm_cancellation_description">¿Seguro que desea detener el proceso KYC? Puede retomarlo en cualquier momento.</string>
|
||||
<string name="tangempay_kyc_failed_description">No pudimos verificar tu perfil. Si tienes alguna pregunta, contacta con el soporte.</string>
|
||||
<string name="tangempay_kyc_failed_title">Lamentablemente, no pudimos verificar tu identidad</string>
|
||||
<string name="tangempay_kyc_has_failed">El proceso KYC ha fallado</string>
|
||||
<string name="tangempay_kyc_in_progress">KYC en curso</string>
|
||||
<string name="tangempay_kyc_in_progress_notification_button">Ver estado</string>
|
||||
<string name="tangempay_kyc_in_progress_notification_title">KYC en progreso para Tangem Pay</string>
|
||||
<string name="tangempay_kyc_in_progress_popup_description">Los documentos suelen verificarse automáticamente en menos de 5 minutos. En casos excepcionales que requieran revisión manual, el proceso puede tardar hasta 48 horas.</string>
|
||||
<string name="tangempay_kyc_rejected">KYC rechazado</string>
|
||||
<string name="tangempay_kyc_rejected_button_text">Ocultar el bloque KYC</string>
|
||||
<string name="tangempay_kyc_rejected_description">Lo sentimos, no pudimos verificarle</string>
|
||||
<string name="tangempay_kyc_rejected_description_span">su perfil.</string>
|
||||
<string name="tangempay_onboarding_banner_description">Obtén tu tarjeta virtual Tangem Visa gratuita</string>
|
||||
<string name="tangempay_onboarding_banner_title">Usa USDC para pagos cotidianos</string>
|
||||
<string name="tangempay_onboarding_get_card_button_text">Obtener tarjeta</string>
|
||||
|
|
@ -1818,7 +1845,17 @@
|
|||
<string name="warning_button_ok">Entendido</string>
|
||||
<string name="warning_button_really_cool">¡Realmente genial!</string>
|
||||
<string name="warning_button_refresh">Actualizar</string>
|
||||
<string name="warning_clore_migration_button">Iniciar migración</string>
|
||||
<string name="warning_clore_migration_copy_button">Copiar</string>
|
||||
<string name="warning_clore_migration_error_signing_not_supported">La firma de mensajes no es compatible con esta red</string>
|
||||
<string name="warning_clore_migration_error_wallet_manager_not_found">No se puede firmar el mensaje. Por favor, inténtelo de nuevo.</string>
|
||||
<string name="warning_clore_migration_message">Según la documentación oficial de Clore, todas las monedas recibidas antes del 21 de diciembre serán migradas a Clore (token ERC-20); las monedas recibidas después de esa fecha no lo serán. Se está desarrollando una solución de transferencia — mantente atento.</string>
|
||||
<string name="warning_clore_migration_message_label">Mensaje</string>
|
||||
<string name="warning_clore_migration_open_portal_button">Abrir el portal de reclamaciones</string>
|
||||
<string name="warning_clore_migration_sheet_description">Para mantener el acceso a sus fondos, comience la migración de acuerdo con las pautas oficiales de Clore.</string>
|
||||
<string name="warning_clore_migration_sheet_title">Migración de la red Clore</string>
|
||||
<string name="warning_clore_migration_sign_button">Firmar</string>
|
||||
<string name="warning_clore_migration_signature_label">Firma</string>
|
||||
<string name="warning_clore_migration_title">Migración de la red Clore</string>
|
||||
<string name="warning_demo_mode_message">Actualmente estás en el modo Demo</string>
|
||||
<string name="warning_demo_mode_title">Modo demo activo</string>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
<string name="account_form_create_button">Ajouter un compte</string>
|
||||
<string name="account_form_edit_button">Enregistrer</string>
|
||||
<string name="account_form_name">Nom du compte</string>
|
||||
<string name="account_form_name_already_exist_error_description">Un compte portant ce nom existe déjà. Veuillez choisir un autre nom.</string>
|
||||
<string name="account_form_name_already_exist_error_title">Nom de compte déjà utilisé</string>
|
||||
<string name="account_form_placeholder_edit_account">Compte</string>
|
||||
<string name="account_form_placeholder_new_account">Nouveau compte</string>
|
||||
|
|
@ -298,6 +299,7 @@
|
|||
<string name="common_go_to_token">Aller au jeton</string>
|
||||
<string name="common_got_it">Compris</string>
|
||||
<string name="common_hide">Cacher</string>
|
||||
<string name="common_hold_to">Maintenir contre %s</string>
|
||||
<string name="common_hour">heure</string>
|
||||
<string name="common_import">Importez</string>
|
||||
<string name="common_in_progress">En cours</string>
|
||||
|
|
@ -322,6 +324,7 @@
|
|||
<string name="common_no">Non</string>
|
||||
<string name="common_no_address">Aucune adresse</string>
|
||||
<string name="common_not_added">Non ajouté</string>
|
||||
<string name="common_not_available">Indisponible</string>
|
||||
<string name="common_not_now">Pas maintenant</string>
|
||||
<string name="common_now">Maintenant</string>
|
||||
<string name="common_ok">OK</string>
|
||||
|
|
@ -372,6 +375,7 @@
|
|||
<string name="common_swap">Échanger</string>
|
||||
<string name="common_tangem">Tangem</string>
|
||||
<string name="common_tangem_wallet">Tangem Wallet</string>
|
||||
<string name="common_tap_and_hold_hint">Appuyez et maintenez enfoncé</string>
|
||||
<string name="common_terms_and_conditions">termes et conditions</string>
|
||||
<string name="common_terms_of_use">Conditions d\'utilisation</string>
|
||||
<string name="common_to">À</string>
|
||||
|
|
@ -399,6 +403,8 @@
|
|||
<string name="common_yield_mode">Mode Rendement</string>
|
||||
<string name="contract_address_copied_message">Adresse du contrat copiée !</string>
|
||||
<string name="currency_subtitle_expanded">Réseaux disponibles</string>
|
||||
<string name="custom_token_another_account_dialog_description">La dérivation de votre token correspond à la dérivation de %1$s. Votre token sera ajouté à ce compte.</string>
|
||||
<string name="custom_token_another_account_dialog_title">La dérivation appartient à un autre compte.</string>
|
||||
<string name="custom_token_another_account_snackbar_text">Jeton ajouté au compte %1$s</string>
|
||||
<string name="custom_token_contract_address_input_title">Adresse du contrat</string>
|
||||
<string name="custom_token_creation_error_invalid_contract_address">L\'adresse du contrat n\'est pas valide</string>
|
||||
|
|
@ -459,6 +465,11 @@
|
|||
<string name="domain_receive_assets_onboarding_description">L’envoi d’actifs sur d’autres réseaux entraînera une perte définitive.</string>
|
||||
<string name="domain_receive_assets_onboarding_network_name">%s réseau</string>
|
||||
<string name="domain_receive_assets_onboarding_title">Envoyez des fonds en utilisant uniquement</string>
|
||||
<string name="earn_best_opportunities">Meilleures opportunités</string>
|
||||
<string name="earn_filter_all_networks">Tous les réseaux</string>
|
||||
<string name="earn_filter_all_types">Tous les types</string>
|
||||
<string name="earn_mostly_used">Principalement utilisé</string>
|
||||
<string name="earn_title">Gagner</string>
|
||||
<string name="email_preface_wc_error">Bonjour équipe de support, j’ai rencontré une erreur avec le code : %s</string>
|
||||
<string name="email_subject_wc_error">Erreur WalletConnect</string>
|
||||
<string name="error_wrong_wallet_tapped">Vous avez utilisé une carte d\'un autre portefeuille. Appuyez sur la carte associée à ce portefeuille</string>
|
||||
|
|
@ -558,6 +569,8 @@
|
|||
<string name="feedback_subject_support_tangem">Commentaires sur Tangem</string>
|
||||
<string name="feedback_subject_tx_failed">Impossible d\'effectuer une transaction</string>
|
||||
<string name="feedback_token_description_error">Erreur de description de la pièce</string>
|
||||
<string name="gasless_not_enough_funds_to_cover_token_fee">Fonds insuffisants</string>
|
||||
<string name="gasless_transaction_fee">Frais de transaction</string>
|
||||
<string name="generic_error">Une erreur s\'est produite</string>
|
||||
<string name="generic_error_code">Une erreur s\'est produite. Code:%s</string>
|
||||
<string name="generic_requires_memo_error">Un mémo est requis</string>
|
||||
|
|
@ -607,6 +620,7 @@
|
|||
<string name="hw_backup_need_title">Finalisez d\'abord la sauvegarde.</string>
|
||||
<string name="hw_backup_no_backup">Incomplet</string>
|
||||
<string name="hw_backup_section_other_title">Autres méthodes</string>
|
||||
<string name="hw_backup_seed_code_description">Conservez votre seed phrase dans un endroit sûr et gardez-la confidentielle afin de protéger vos fonds, puis configurez un code d\'accès pour plus de sécurité.</string>
|
||||
<string name="hw_backup_seed_description">Conservez votre seed phrase dans un endroit sûr et gardez-la confidentielle afin de protéger vos fonds.</string>
|
||||
<string name="hw_backup_seed_title">Seed phrase</string>
|
||||
<string name="hw_backup_to_secure_description">Pour sécuriser votre wallet avec un code d\'accès, effectuez la procédure de sauvegarde.</string>
|
||||
|
|
@ -644,7 +658,7 @@
|
|||
<string name="hw_upgrade_error_wallet2_card_required">Une erreur s\'est produite pendant l\'opération.</string>
|
||||
<string name="hw_upgrade_funds_access_description">Vos fonds restent en sécurité et entièrement accessibles pendant toute la durée du processus.</string>
|
||||
<string name="hw_upgrade_funds_access_title">Accès aux fonds</string>
|
||||
<string name="hw_upgrade_general_security_description">Après la mise à niveau, votre portefeuille mobile sera supprimé de l\'application et stocké sur votre hardware wallet; votre seedphrase restera en votre possession.</string>
|
||||
<string name="hw_upgrade_general_security_description">Après la mise à niveau, votre portefeuille mobile sera supprimé de l\'application et stocké sur votre hardware wallet; votre seed phrase restera en votre possession.</string>
|
||||
<string name="hw_upgrade_general_security_title">Sécurité générale</string>
|
||||
<string name="hw_upgrade_key_migration_description">Les clés privées seront transférées de l\'application vers votre hardware wallet Tangem.</string>
|
||||
<string name="hw_upgrade_key_migration_title">Migration des clés</string>
|
||||
|
|
@ -893,6 +907,9 @@
|
|||
<string name="notification_sepa_title">Acheter des cryptomonnaies avec SEPA</string>
|
||||
<string name="notification_visa_waitlist_promo_text">Rejoignez la liste d’attente et obtenez une carte de paiement pas comme les autres.</string>
|
||||
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
|
||||
<string name="notification_yield_promo_button">Conditions générales</string>
|
||||
<string name="notification_yield_promo_text">Déposez 100 $ ou plus, conservez le dépôt pendant 30 jours et recevez 10 $.</string>
|
||||
<string name="notification_yield_promo_title">Rejoignez la campagne Yield Mode !</string>
|
||||
<string name="onboarding_access_code_feature_1_description">Vous devez définir un seul code d\'accès pour protéger tous vos appareils.</string>
|
||||
<string name="onboarding_access_code_feature_1_title">Protéger</string>
|
||||
<string name="onboarding_access_code_feature_2_description">Vous pourrez définir un code d\'accès individuel sur chaque carte plus tard</string>
|
||||
|
|
@ -992,6 +1009,7 @@
|
|||
<string name="onboarding_wallet_info_title_fourth">Restauration du code d\'accès</string>
|
||||
<string name="onboarding_wallet_info_title_second">Cartes identiques</string>
|
||||
<string name="onboarding_wallet_info_title_third">Code d\'accès</string>
|
||||
<string name="only_one_mobile_wallet_explanation">Vous ne pouvez avoir qu\'un seul portefeuille mobile. Passez au portefeuille matériel Tangem ou ajoutez un nouveau portefeuille matériel.</string>
|
||||
<string name="onramp_all_offers_button_title">Toutes les offres</string>
|
||||
<string name="onramp_avaiable_with_payment_methods">Disponible avec %s</string>
|
||||
<string name="onramp_choose_provider_title_hint">L\'achat de crypto-monnaies sur Tangem est assuré par des fournisseurs tiers selon leurs conditions.</string>
|
||||
|
|
@ -1036,6 +1054,7 @@
|
|||
<string name="onramp_title_available_from">Disponible auprès de</string>
|
||||
<string name="onramp_title_available_up_to">Disponible jusqu\'à</string>
|
||||
<string name="onramp_title_you_get">Vous obtenez</string>
|
||||
<string name="onramp_tos_external_providers">Le service est fourni par un prestataire externe. Tangem n\'est pas responsable.</string>
|
||||
<string name="onramp_transaction_status_footer_text">Vous pouvez vérifier l\'état de la transaction à partir de la page du jeton</string>
|
||||
<string name="onramp_up_to_rate">Jusqu\'à</string>
|
||||
<string name="onramp_via">Via</string>
|
||||
|
|
@ -1248,18 +1267,21 @@
|
|||
<string name="settings_forget_wallet_footer">Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau.</string>
|
||||
<string name="settings_upgrade_banner_advantage_feature_title">Facile à utiliser</string>
|
||||
<string name="settings_upgrade_banner_description">Gardez vos cryptomonnaies en sécurité et hors ligne. Aussi fin qu\'une carte de crédit, plus sûr qu\'un coffre-fort bancaire.</string>
|
||||
<string name="settings_upgrade_banner_seed_phrase_feature_title">Pas de seedphrase</string>
|
||||
<string name="settings_upgrade_banner_seed_phrase_feature_title">Pas de seed phrase</string>
|
||||
<string name="settings_upgrade_banner_superiority_feature_title">Le meilleur hardware wallet</string>
|
||||
<string name="settings_upgrade_banner_title">Tangem Hardware Wallet</string>
|
||||
<string name="settings_wallet_name_title">Nom</string>
|
||||
<string name="stake_token_description">Faites travailler votre jeton pour vous</string>
|
||||
<string name="staking_account_initialization_footer">Les frais de réseau sont une petite somme à payer pour traiter et confirmer ta transaction sur la blockchain.</string>
|
||||
<string name="staking_account_initialization_message">Pour commencer à staker, votre compte TON doit être activé avec une transaction de 1 TON. Les fonds restent sur votre compte, car cette étape sert uniquement à l\'activer pour le staking.</string>
|
||||
<string name="staking_account_initialization_title">Activation du compte</string>
|
||||
<string name="staking_alert_network_fee_updated_message">Les frais de réseau ont changé. Veuillez vérifier le nouveau montant avant de continuer.</string>
|
||||
<string name="staking_alert_network_fee_updated_title">Frais de réseau mis à jour</string>
|
||||
<string name="staking_amount_requirement_error">Le montant à staker doit être au moins %s</string>
|
||||
<string name="staking_amount_tron_integer_error">Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau.</string>
|
||||
<string name="staking_amount_tron_integer_error_unstaking">Le montant d\'annulation du staking sera arrondi à %1$s TRX en raison des règles du réseau.</string>
|
||||
<string name="staking_apr_earn_badge">TAEG %1$s%%</string>
|
||||
<string name="staking_cardano_details_rewards_info_text">Vos récompenses de staking commenceront après 5 époques (~25 jours), une fois que votre délégation aura été enregistrée et comptabilisée par le réseau.</string>
|
||||
<string name="staking_claim_unstaked">Réclamation déstakée</string>
|
||||
<string name="staking_details_account_fee">Frais de staking du compte</string>
|
||||
<string name="staking_details_account_fee_info">Un compte de staking est un compte spécial où sont stockés les jetons SOL stakés. Il est créé lorsque vous déléguez vos jetons à un validateur pour participer à la validation des transactions et gagner des récompenses. Des frais minimes sont facturés pour la création du compte de staking, qui sont restitués une fois le staking terminé.</string>
|
||||
|
|
@ -1270,6 +1292,7 @@
|
|||
<string name="staking_details_apr">APR</string>
|
||||
<string name="staking_details_apy">APY</string>
|
||||
<string name="staking_details_auto_claiming_rewards_daily_text">Les récompenses s\'accumulent automatiquement sur votre solde de staking quotidiennement.</string>
|
||||
<string name="staking_details_autocompound_rewards_earned">Les récompenses sont ajoutées à votre solde de staking. Fonds gagnés : %s</string>
|
||||
<string name="staking_details_available">Disponible</string>
|
||||
<string name="staking_details_average_reward_rate">Taux de récompense moyen</string>
|
||||
<string name="staking_details_banner_text">Qu\'est-ce que le Staking ?</string>
|
||||
|
|
@ -1325,6 +1348,7 @@
|
|||
<string name="staking_notification_restake_text">L\'option de restaker vous permet de déplacer vos fonds d\'un validateur à un autre sans avoir besoin de les déstaker.</string>
|
||||
<string name="staking_notification_stake_entire_balance_text">Vous êtes sur le point de staker l\'intégralité de votre solde. Nous vous recommandons de laisser un petit montant pour couvrir les frais de réseau pour unstaking ou la réclamation des récompenses.</string>
|
||||
<string name="staking_notification_ton_account_initialization_message">Pour commencer le staking, vous devez d’abord activer votre compte TON</string>
|
||||
<string name="staking_notification_ton_account_initialization_title">Activation du compte</string>
|
||||
<string name="staking_notification_ton_activate_account">Pour commencer à staker TON, effectuez d\'abord une transaction sortante de n\'importe quel montant — cela activera votre portefeuille.</string>
|
||||
<string name="staking_notification_ton_extra_reserve_info">Jusqu\'à 0,2 TON peuvent être requis en plus des frais de réseau pour terminer la transaction. Tout montant non utilisé sera remboursé.</string>
|
||||
<string name="staking_notification_ton_extra_reserve_is_required">0,2 TON requis pour effectuer cette opération, en plus des frais de réseau. Veuillez recharger votre solde.</string>
|
||||
|
|
@ -1413,6 +1437,7 @@
|
|||
<string name="swap_story_third_subtitle">Simple et intuitif, vous permettant d\'échanger des jetons en quelques clics</string>
|
||||
<string name="swap_story_third_title">Plus Simple Que Jamais</string>
|
||||
<string name="swap_via_provider">Échange via le fournisseur</string>
|
||||
<string name="swap_your_assets_title">Vos actifs</string>
|
||||
<string name="swapping_alert_cex_description">Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %s depuis l\'échange vers l\'adresse de l\'utilisateur.</string>
|
||||
<string name="swapping_alert_cex_description_with_slippage">Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %1$s depuis l\'échange vers l\'adresse de l\'utilisateur. \n\nLe slippage du fournisseur peut atteindre %2$s</string>
|
||||
<string name="swapping_alert_dex_description">Le montant comprend les frais du fournisseur de services.</string>
|
||||
|
|
@ -1427,6 +1452,7 @@
|
|||
<string name="swapping_insufficient_funds">Fonds insuffisants</string>
|
||||
<string name="swapping_permission_header">Donner l\'autorisation</string>
|
||||
<string name="swapping_swap_action">Échanger</string>
|
||||
<string name="swapping_swap_action_in_progress">Échange...</string>
|
||||
<string name="swapping_to_title">Vous recevez</string>
|
||||
<string name="swapping_token_list_title">Choisir le jeton</string>
|
||||
<string name="swapping_token_not_available">non disponible</string>
|
||||
|
|
@ -1445,6 +1471,9 @@
|
|||
<string name="tangem_pay_freeze_card_freeze">Geler</string>
|
||||
<string name="tangem_pay_freeze_card_success">Votre carte est gelée.</string>
|
||||
<string name="tangem_pay_get_help">Obtenir de l\'aide</string>
|
||||
<string name="tangem_pay_history_item_spend_mc_declined_reason">Raison: %s</string>
|
||||
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s · %s</string>
|
||||
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
|
||||
<string name="tangem_pay_other">Autre</string>
|
||||
<string name="tangem_pay_rooted_device_subtitle">Impossible à utiliser sur les appareils rootés</string>
|
||||
<string name="tangem_pay_status_completed">Terminé</string>
|
||||
|
|
@ -1521,10 +1550,15 @@
|
|||
<string name="tangempay_kyc_confirm_cancellation_description">Êtes-vous sûr de vouloir interrompre le processus KYC ? Vous pouvez y revenir à tout moment.</string>
|
||||
<string name="tangempay_kyc_failed_description">Nous n\'avons pas pu vérifier votre profil. Pour toute question, veuillez contacter le support.</string>
|
||||
<string name="tangempay_kyc_failed_title">Malheureusement, nous n\'avons pas pu vérifier votre identité</string>
|
||||
<string name="tangempay_kyc_has_failed">KYC a échoué</string>
|
||||
<string name="tangempay_kyc_in_progress">KYC en cours</string>
|
||||
<string name="tangempay_kyc_in_progress_notification_button">Voir le statut</string>
|
||||
<string name="tangempay_kyc_in_progress_notification_title">KYC en cours pour Tangem Pay</string>
|
||||
<string name="tangempay_kyc_in_progress_popup_description">Les documents sont généralement vérifiés automatiquement en moins de 5 minutes. Dans de rares cas nécessitant une vérification manuelle, le processus peut prendre jusqu’à 48 heures.</string>
|
||||
<string name="tangempay_kyc_rejected">KYC rejeté</string>
|
||||
<string name="tangempay_kyc_rejected_button_text">Masquer le bloc KYC</string>
|
||||
<string name="tangempay_kyc_rejected_description">Désolé, nous n\'avons pas pu vérifier.</string>
|
||||
<string name="tangempay_kyc_rejected_description_span">votre profil.</string>
|
||||
<string name="tangempay_onboarding_banner_description">Obtenez votre carte virtuelle Tangem Visa gratuite</string>
|
||||
<string name="tangempay_onboarding_banner_title">Utilisez USDC pour les paiements quotidiens</string>
|
||||
<string name="tangempay_onboarding_get_card_button_text">Obtenir la carte</string>
|
||||
|
|
@ -1579,6 +1613,7 @@
|
|||
<string name="token_details_token_type_subtitle_no_standard">Jeton dans le %%image%% %1$s réseau</string>
|
||||
<string name="token_details_unable_hide_alert_message">Le jeton %1$s (%2$s) est la principale devise du réseau %3$s et ne peut pas être masqué tant que vous avez d\'autres jetons de ce réseau dans la liste</string>
|
||||
<string name="token_details_unable_hide_alert_title">Impossible de masquer %s</string>
|
||||
<string name="token_receive_show_qr_code_title">Afficher le code QR</string>
|
||||
<string name="token_swap_changelly_promotion_message">Échangez ce jeton contre un autre moyennant des frais de service de %1$s du %2$s au %3$s février.</string>
|
||||
<string name="token_swap_changelly_promotion_title">Échangez avec Changelly, %s frais</string>
|
||||
<string name="token_swap_promotion_button">Échangez maintenant</string>
|
||||
|
|
@ -1710,6 +1745,12 @@
|
|||
<string name="visa_unlock_notification_subtitle">Scannez votre carte pour déverrouiller l\'accès</string>
|
||||
<string name="visa_unlock_notification_title">Déverrouillage nécessaire</string>
|
||||
<string name="wallet_add_common_title">Choisissez comment ajouter votre portefeuille</string>
|
||||
<string name="wallet_add_hardware_description">Scannez votre carte ou votre bague Tangem pour la restaurer ou l\'importer depuis un autre portefeuille.</string>
|
||||
<string name="wallet_add_hardware_info_create">Créer un Hardware Wallet</string>
|
||||
<string name="wallet_add_hardware_purchase">Vous souhaitez acheter un portefeuille Tangem ?</string>
|
||||
<string name="wallet_add_import_seed_phrase">Importer la seed phrase</string>
|
||||
<string name="wallet_add_mobile_description">Restaurez votre portefeuille sur votre téléphone ou importez-le depuis une autre application — pratique, mais moins sécurisé qu\'une carte Tangem.</string>
|
||||
<string name="wallet_add_support_title">Que choisir ?</string>
|
||||
<string name="wallet_balance_blockchain_unreachable_try_later">La blockchain n\'est pas accessible. Réessayez plus tard</string>
|
||||
<string name="wallet_balance_missing_derivation">Scanner la carte ou la bague</string>
|
||||
<string name="wallet_been_activated_message">Ce portefeuille a déjà été activé auparavant.\nSi cela n\'a pas été fait par vous, veuillez contacter le support.\nTangem ne vend jamais de portefeuilles avec le code d\'accès pré-généré.</string>
|
||||
|
|
@ -1766,9 +1807,9 @@
|
|||
<string name="wallet_import_navtitle">Ajouter un portefeuille existant</string>
|
||||
<string name="wallet_import_scan_description">Dispositifs physiques qui stockent votre clé privée hors ligne en toute sécurité.</string>
|
||||
<string name="wallet_import_scan_title">Scanner un Tangem Wallet</string>
|
||||
<string name="wallet_import_seed_description">Importez un portefeuille existant à l\'aide de votre seedphrase.</string>
|
||||
<string name="wallet_import_seed_description">Importez un portefeuille existant à l\'aide de votre seed phrase.</string>
|
||||
<string name="wallet_import_seed_navtitle">Importer un wallet</string>
|
||||
<string name="wallet_import_seed_title">Entrez la seedphrase</string>
|
||||
<string name="wallet_import_seed_title">Entrez la seed phrase</string>
|
||||
<string name="wallet_import_success_description">Vous avez importé votre portefeuille avec succès.</string>
|
||||
<string name="wallet_import_success_navtitle">Importer un wallet</string>
|
||||
<string name="wallet_import_success_title">Importation terminée</string>
|
||||
|
|
@ -1984,7 +2025,7 @@
|
|||
<string name="welcome_create_wallet_feature_delivery">Livraison rapide</string>
|
||||
<string name="welcome_create_wallet_feature_one_tap">Commencez en un seul clic</string>
|
||||
<string name="welcome_create_wallet_feature_seamless">Sans faille et sécurisé</string>
|
||||
<string name="welcome_create_wallet_feature_seedphrase">Pas de seedphrase</string>
|
||||
<string name="welcome_create_wallet_feature_seedphrase">Pas de seed phrase</string>
|
||||
<string name="welcome_create_wallet_feature_use">Facile à utiliser</string>
|
||||
<string name="welcome_create_wallet_hardware_description">Créez un harware wallet avec Tangem. Aussi fin qu\'une carte bancaire, aussi sûr qu\'un coffre-fort.</string>
|
||||
<string name="welcome_create_wallet_mobile_description">Créer ou importer un portefeuille logiciel</string>
|
||||
|
|
@ -2105,4 +2146,5 @@
|
|||
<string name="yield_module_unable_to_cover_fee_title">Impossible de couvrir les frais %s</string>
|
||||
<string name="yield_module_unavailable_subtitle">Le mode rendement n\'est pas disponible pour le moment. Veuillez réessayer plus tard.</string>
|
||||
<string name="yield_module_unavailable_title">Le mode Rendement est indisponible</string>
|
||||
<string name="yield_supply_chart_loading_error">Impossible de charger le graphique...</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -294,6 +294,7 @@
|
|||
<string name="common_go_to_token">トークンへ移動</string>
|
||||
<string name="common_got_it">わかりました</string>
|
||||
<string name="common_hide">非表示</string>
|
||||
<string name="common_hold_to">%sまで長押し</string>
|
||||
<string name="common_hour">時間</string>
|
||||
<string name="common_import">インポート</string>
|
||||
<string name="common_in_progress">進行中</string>
|
||||
|
|
@ -368,6 +369,7 @@
|
|||
<string name="common_swap">スワップ</string>
|
||||
<string name="common_tangem">Tangem</string>
|
||||
<string name="common_tangem_wallet">Tangem Wallet</string>
|
||||
<string name="common_tap_and_hold_hint">タップして長押し</string>
|
||||
<string name="common_terms_and_conditions">利用規約</string>
|
||||
<string name="common_terms_of_use">利用規約</string>
|
||||
<string name="common_to">宛先</string>
|
||||
|
|
@ -456,6 +458,11 @@
|
|||
<string name="domain_receive_assets_onboarding_description">他のネットワークで資産を送金すると、永久に失われます。</string>
|
||||
<string name="domain_receive_assets_onboarding_network_name">%sネットワーク</string>
|
||||
<string name="domain_receive_assets_onboarding_title">下記のみを使用して資金を送金する</string>
|
||||
<string name="earn_best_opportunities">おすすめ</string>
|
||||
<string name="earn_filter_all_networks">すべてのネットワーク</string>
|
||||
<string name="earn_filter_all_types">すべての種類</string>
|
||||
<string name="earn_mostly_used">よく使われています</string>
|
||||
<string name="earn_title">運用</string>
|
||||
<string name="email_preface_wc_error">こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。</string>
|
||||
<string name="email_subject_wc_error">WalletConnectエラー</string>
|
||||
<string name="error_wrong_wallet_tapped">別のウォレットのカードまたはリングを使用しました。このウォレットにリンクしているカードまたはリングをタップしてください。</string>
|
||||
|
|
@ -887,6 +894,9 @@
|
|||
<string name="notification_sepa_title">SEPAで暗号資産を購入</string>
|
||||
<string name="notification_visa_waitlist_promo_text">事前申し込み受付中。他にない特別なカードを、いち早く体験しよう。</string>
|
||||
<string name="notification_visa_waitlist_promo_title">Tangem Visaカード</string>
|
||||
<string name="notification_yield_promo_button">利用規約</string>
|
||||
<string name="notification_yield_promo_text">100ドル以上を入金し、30日間保持すると、10ドルを受け取れます。</string>
|
||||
<string name="notification_yield_promo_title">Yield Mode キャンペーンに参加しよう!</string>
|
||||
<string name="onboarding_access_code_feature_1_description">すべてのデバイスを保護するには、単一のアクセスコードを設定してください。</string>
|
||||
<string name="onboarding_access_code_feature_1_title">保護する</string>
|
||||
<string name="onboarding_access_code_feature_2_description">後で各カードおよびリングに個別のアクセスコードを設定できます。</string>
|
||||
|
|
@ -935,11 +945,11 @@
|
|||
<string name="onboarding_navbar_upgrade_wallet_biometrics">生体認証</string>
|
||||
<string name="onboarding_seed_button_read_more">シードフレーズについてもっと読む</string>
|
||||
<plurals name="onboarding_seed_generate_message_words_count">
|
||||
<item quantity="other">これらの %d 単語を以下の順番通りに書き留め、安全かつ秘密の場所に保管してください。</item>
|
||||
<item quantity="other">以下に表示される%d個の単語を順番どおりに書き留め、安全で他人に知られない場所に保管してください。</item>
|
||||
</plurals>
|
||||
<string name="onboarding_seed_generate_title">あなたのシードフレーズ</string>
|
||||
<plurals name="onboarding_seed_generate_words_count">
|
||||
<item quantity="other">%d 単語</item>
|
||||
<item quantity="other">%d個の単語</item>
|
||||
</plurals>
|
||||
<string name="onboarding_seed_import_message">ウォレットをインポートするには、下のフィールドにシードフレーズを入力してください。</string>
|
||||
<string name="onboarding_seed_intro_button_generate">シードフレーズを生成する</string>
|
||||
|
|
@ -1252,6 +1262,7 @@
|
|||
<string name="staking_amount_tron_integer_error">ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。</string>
|
||||
<string name="staking_amount_tron_integer_error_unstaking">ネットワークルールにより、ステーキング解除の量は%1$s TRX に切り上げられます。</string>
|
||||
<string name="staking_apr_earn_badge">APR %1$s%%</string>
|
||||
<string name="staking_cardano_details_rewards_info_text">デリゲーションがネットワークに登録され、集計されるまでの間、ステーキング報酬は5エポック後(約25日後)から開始されます。</string>
|
||||
<string name="staking_claim_unstaked">ステーキング解除分を請求する</string>
|
||||
<string name="staking_details_account_fee">ステーキングアカウント手数料</string>
|
||||
<string name="staking_details_account_fee_info">ステーキングアカウントとは、ステーキングされたSOLが保管される特別なアカウントです。トークンをバリデーターに委任し、取引の検証に参加して報酬を受け取る際に作成されます。ステーキングアカウントの作成には少額の手数料がかかりますが、ステーキング完了後に返金されます。</string>
|
||||
|
|
@ -1441,6 +1452,9 @@
|
|||
<string name="tangem_pay_freeze_card_freeze">一時停止</string>
|
||||
<string name="tangem_pay_freeze_card_success">カードが凍結されています</string>
|
||||
<string name="tangem_pay_get_help">サポートを受ける</string>
|
||||
<string name="tangem_pay_history_item_spend_mc_declined_reason">理由:%s</string>
|
||||
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s・%s</string>
|
||||
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
|
||||
<string name="tangem_pay_other">その他</string>
|
||||
<string name="tangem_pay_rooted_device_subtitle">Root化された端末では使用できません</string>
|
||||
<string name="tangem_pay_status_completed">完了</string>
|
||||
|
|
@ -1643,25 +1657,69 @@
|
|||
<string name="user_wallet_list_rename_popup_title">ウォレット名の変更</string>
|
||||
<string name="user_wallet_list_unlock_all">すべてのロックを解除</string>
|
||||
<string name="user_wallet_list_unlock_all_with">%sですべてをロック解除</string>
|
||||
<string name="visa_balance_limits_details_aml_verified">AML認証済み</string>
|
||||
<string name="visa_balance_limits_details_available">利用可能</string>
|
||||
<string name="visa_balance_limits_details_blocked">ブロック中</string>
|
||||
<string name="visa_balance_limits_details_debt">負債</string>
|
||||
<string name="visa_balance_limits_details_limits">利用上限</string>
|
||||
<string name="visa_balance_limits_details_no_otp_limit">その他(OTPなし)</string>
|
||||
<string name="visa_balance_limits_details_single_transaction">単一の取引</string>
|
||||
<string name="visa_balance_limits_details_total">合計</string>
|
||||
<plurals name="visa_limits_available_for_days_title">
|
||||
<item quantity="other">%d日間利用可能</item>
|
||||
</plurals>
|
||||
<string name="visa_main_available_balance_alert_message">利用可能残高は、保留中の取引、ブロック中の金額、デビット残高を考慮した、実際に利用できる資金を示します。</string>
|
||||
<string name="visa_main_available_till_date">%1$sまで利用可能</string>
|
||||
<string name="visa_main_balances_and_limits">残高と限度額</string>
|
||||
<string name="visa_main_button_alert_missing_payment_account_info_message">支払いアカウントの情報が見つかりません。サポートにお問い合わせください。</string>
|
||||
<string name="visa_main_button_alert_missing_token_info">トークンの情報を読み込めません。</string>
|
||||
<string name="visa_main_button_alert_still_loading">情報を読み込み中です。しばらくお待ちください。</string>
|
||||
<string name="visa_main_limits_alert_description_message">コストの管理、セキュリティの向上、リスク管理のために利用上限が設けられています。店舗でのカード決済では期間内に%1$s、サブスクリプションや支払いなどのその他の取引では%2$sまで利用できます。</string>
|
||||
<string name="visa_onboarding_access_code_description">アクセスコードは、支払いアカウントの管理および不正アクセスからの保護に使用されます。</string>
|
||||
<string name="visa_onboarding_access_code_navigation_title">アクセスコード</string>
|
||||
<string name="visa_onboarding_account_activation_navigation_title">アカウント有効化</string>
|
||||
<string name="visa_onboarding_approve_wallet_selector_notification_message">登録手続きを開始したウォレットを選択し、ブロックチェーン上でアカウントを作成するための取引に署名してください。</string>
|
||||
<string name="visa_onboarding_cancel_activation">有効化をキャンセル</string>
|
||||
<string name="visa_onboarding_close_alert_message">本当に終了してもよろしいですか?中断したところから後で続行できます。</string>
|
||||
<string name="visa_onboarding_in_progress_description">長くはかかりません。アカウントを設定しています。</string>
|
||||
<string name="visa_onboarding_in_progress_issuer_description">長くはかかりません。アクティベーションを完了しています。</string>
|
||||
<string name="visa_onboarding_in_progress_title">準備完了です!</string>
|
||||
<string name="visa_onboarding_other_wallet">その他のウォレット</string>
|
||||
<string name="visa_onboarding_pin_code_description">4桁のコードを設定します。 \nお支払いの際に使用されます。</string>
|
||||
<string name="visa_onboarding_pin_code_navigation_title">PINコード</string>
|
||||
<string name="visa_onboarding_pin_code_title">PINコードを作成する</string>
|
||||
<string name="visa_onboarding_pin_not_accepted">PINの認証に失敗しました。もう一度お試しいただくか、別のコードを使用してください。</string>
|
||||
<string name="visa_onboarding_pin_validation_error_message">無効な暗証番号:連続や繰り返しを避けてください</string>
|
||||
<string name="visa_onboarding_success_screen_description">準備完了です!</string>
|
||||
<string name="visa_onboarding_tangem_approve_description">Tangemカードを用意し、タップして承認してください。</string>
|
||||
<string name="visa_onboarding_tangem_approve_title">Tangemウォレットを準備してください。</string>
|
||||
<string name="visa_onboarding_wallet_connect_description">\nサードパーティのWebサイト\nで接続手続きを完了し、その後Tangemアプリに戻ってください。</string>
|
||||
<string name="visa_onboarding_wallet_connect_title">ウェブサイトに移動</string>
|
||||
<string name="visa_onboarding_wallet_connection_navigation_title">ウォレット接続</string>
|
||||
<string name="visa_onboarding_wallet_list_header">ウォレットを選択</string>
|
||||
<string name="visa_onboarding_welcome_back_button_title">有効化を続行</string>
|
||||
<string name="visa_onboarding_welcome_back_description">アカウントの設定を続けましょう。</string>
|
||||
<string name="visa_onboarding_welcome_back_title">お帰りなさい!</string>
|
||||
<string name="visa_onboarding_welcome_button_title">有効化を開始</string>
|
||||
<string name="visa_onboarding_welcome_description">手順に従ってアカウントを設定してください。</string>
|
||||
<string name="visa_onboarding_welcome_title">ようこそ!</string>
|
||||
<string name="visa_transaction_details_blockchain_amount">ブロックチェーン上の金額</string>
|
||||
<string name="visa_transaction_details_currency_code">通貨コード</string>
|
||||
<string name="visa_transaction_details_date">日付</string>
|
||||
<string name="visa_transaction_details_error_code">エラーコード</string>
|
||||
<string name="visa_transaction_details_header">取引詳細</string>
|
||||
<string name="visa_transaction_details_merchant_category_code">加盟店カテゴリーコード</string>
|
||||
<string name="visa_transaction_details_merchant_city">加盟店の所在地</string>
|
||||
<string name="visa_transaction_details_merchant_country_code">加盟店の国コード</string>
|
||||
<string name="visa_transaction_details_merchant_name">加盟店名</string>
|
||||
<string name="visa_transaction_details_request_id">リクエストID</string>
|
||||
<string name="visa_transaction_details_status">ステータス</string>
|
||||
<string name="visa_transaction_details_title">取引</string>
|
||||
<string name="visa_transaction_details_transaction_amount">取引金額</string>
|
||||
<string name="visa_transaction_details_transaction_hash">トランザクションハッシュ</string>
|
||||
<string name="visa_transaction_details_transaction_request">取引リクエスト</string>
|
||||
<string name="visa_transaction_details_transaction_status">取引ステータス</string>
|
||||
<string name="visa_transaction_details_type">種類</string>
|
||||
<string name="visa_tx_dispute_button">この取引に異議を唱える</string>
|
||||
<string name="visa_unlock_notification_button">ロック解除</string>
|
||||
<string name="visa_unlock_notification_subtitle">カードをスキャンしてアクセスロックを解除する</string>
|
||||
|
|
|
|||
|
|
@ -925,6 +925,9 @@
|
|||
<string name="notification_sepa_title">Покупайте крипту через SEPA</string>
|
||||
<string name="notification_visa_waitlist_promo_text">Присоединяйтесь к списку ожидания и получите уникальную платёжную карту, не похожую ни на одну другую.</string>
|
||||
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
|
||||
<string name="notification_yield_promo_button">Условия программы</string>
|
||||
<string name="notification_yield_promo_text">Внесите депозит от $100, удерживайте его 30 дней и получите бонус $10</string>
|
||||
<string name="notification_yield_promo_title">Подключите Режим доходности!</string>
|
||||
<string name="onboarding_access_code_feature_1_description">Установите единый код доступа для защиты всех ваших карт или колец</string>
|
||||
<string name="onboarding_access_code_feature_1_title">Защита</string>
|
||||
<string name="onboarding_access_code_feature_2_description">Установите индивидуальный код доступа для каждой карты или кольца позже.</string>
|
||||
|
|
@ -1800,7 +1803,17 @@
|
|||
<string name="warning_button_ok">Понятно!</string>
|
||||
<string name="warning_button_really_cool">Очень круто!</string>
|
||||
<string name="warning_button_refresh">Обновить</string>
|
||||
<string name="warning_clore_migration_button">Начать миграцию</string>
|
||||
<string name="warning_clore_migration_copy_button">Копировать</string>
|
||||
<string name="warning_clore_migration_error_signing_not_supported">Подписание сообщений не поддерживается в этой сети</string>
|
||||
<string name="warning_clore_migration_error_wallet_manager_not_found">Невозможно подписать сообщение. Пожалуйста, попробуй позже.</string>
|
||||
<string name="warning_clore_migration_message">Согласно официальной документации Clore, все монеты, полученные до 21 декабря, будут мигрированы в токен Clore (ERC-20); монеты, полученные после этой даты, — нет. Решение для перевода находится в разработке — следите за обновлениями.</string>
|
||||
<string name="warning_clore_migration_message_label">Сообщение</string>
|
||||
<string name="warning_clore_migration_open_portal_button">Открыть портал клейма</string>
|
||||
<string name="warning_clore_migration_sheet_description">Чтобы сохранить доступ к своим средствам, начните миграцию в соответствии с официальной инструкцией Clore.</string>
|
||||
<string name="warning_clore_migration_sheet_title">Миграция сети Clore</string>
|
||||
<string name="warning_clore_migration_sign_button">Подписать</string>
|
||||
<string name="warning_clore_migration_signature_label">Подпись</string>
|
||||
<string name="warning_clore_migration_title">Миграция сети Clore</string>
|
||||
<string name="warning_demo_mode_message">Вы находитесь в режиме демо</string>
|
||||
<string name="warning_demo_mode_title">Демо режим включен</string>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
<string name="account_form_edit_button">Зберегти</string>
|
||||
<string name="account_form_name">Назва акаунту</string>
|
||||
<string name="account_form_name_already_exist_error_description">Акаунт з такою назвою вже існує. Будь ласка, виберіть іншу назву.</string>
|
||||
<string name="account_form_name_already_exist_error_title">Обліковий запис з таким ім’ям уже існує</string>
|
||||
<string name="account_form_placeholder_edit_account">Акаунт</string>
|
||||
<string name="account_form_placeholder_new_account">Новий акаунт</string>
|
||||
<string name="account_form_title_create">Додати акаунт</string>
|
||||
|
|
@ -908,6 +909,9 @@
|
|||
<string name="notification_sepa_title">Купуйте криптовалюту через SEPA</string>
|
||||
<string name="notification_visa_waitlist_promo_text">Приєднуйтесь до списку очікування та отримайте унікальну платіжну картку, якої ще не було.</string>
|
||||
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
|
||||
<string name="notification_yield_promo_button">Умови та положення</string>
|
||||
<string name="notification_yield_promo_text">Внесіть депозит від $100, утримуйте його 30 днів та отримайте $10.</string>
|
||||
<string name="notification_yield_promo_title">Кампанія Yield Mode!</string>
|
||||
<string name="onboarding_access_code_feature_1_description">Налаштуйте єдиний код доступу для захисту всіх ваших карток або кілець</string>
|
||||
<string name="onboarding_access_code_feature_1_title">Захист</string>
|
||||
<string name="onboarding_access_code_feature_2_description">Встановіть індивідуальний код доступу для кожної картки або кільця пізніше.</string>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@
|
|||
<string name="address_qr_code_message_format">Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||
<string name="address_type_default">Default</string>
|
||||
<string name="address_type_legacy">Legacy</string>
|
||||
<string name="alert_authentication_error_message">Try to reset biometrics on your device or contact support</string>
|
||||
<string name="alert_authentication_error_title">Authentication error</string>
|
||||
<string name="alert_button_how_to_scan">How to scan</string>
|
||||
<string name="alert_button_request_support">Request support</string>
|
||||
<string name="alert_button_try_again">Try again</string>
|
||||
|
|
@ -908,6 +910,9 @@
|
|||
<string name="notification_sepa_title">Buy Crypto with SEPA</string>
|
||||
<string name="notification_visa_waitlist_promo_text">Join the waitlist and get a payment card unlike any other</string>
|
||||
<string name="notification_visa_waitlist_promo_title">Tangem Visa Card</string>
|
||||
<string name="notification_yield_promo_button">Terms and conditions</string>
|
||||
<string name="notification_yield_promo_text">Deposit $100+, hold for 30 days, get $10</string>
|
||||
<string name="notification_yield_promo_title">Join the Yield Mode Campaign!</string>
|
||||
<string name="onboarding_access_code_feature_1_description">Set up a single access code to protect all your devices.</string>
|
||||
<string name="onboarding_access_code_feature_1_title">Protect</string>
|
||||
<string name="onboarding_access_code_feature_2_description">Set an individual access code for each card or ring later.</string>
|
||||
|
|
@ -1843,7 +1848,18 @@
|
|||
<string name="warning_button_ok">Ok, Got it!</string>
|
||||
<string name="warning_button_really_cool">Really cool!</string>
|
||||
<string name="warning_button_refresh">Refresh</string>
|
||||
<string name="warning_clore_migration_button">Start migration</string>
|
||||
<string name="warning_clore_migration_copy_button">Copy</string>
|
||||
<string name="warning_clore_migration_description">To keep access to your funds, start the migration according to the official Clore guidelines.</string>
|
||||
<string name="warning_clore_migration_error_signing_not_supported">Message signing is not supported for this network</string>
|
||||
<string name="warning_clore_migration_error_wallet_manager_not_found">Unable to sign message. Please try again.</string>
|
||||
<string name="warning_clore_migration_message">According to 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.</string>
|
||||
<string name="warning_clore_migration_message_label">Message</string>
|
||||
<string name="warning_clore_migration_open_portal_button">Open Claim portal</string>
|
||||
<string name="warning_clore_migration_sheet_description">To continue using your Clore tokens, you must complete the token migration according to the information on the Claim Portal.</string>
|
||||
<string name="warning_clore_migration_sheet_title">Clore Network Migration</string>
|
||||
<string name="warning_clore_migration_sign_button">Sign</string>
|
||||
<string name="warning_clore_migration_signature_label">Signature</string>
|
||||
<string name="warning_clore_migration_title">Clore Network Migration</string>
|
||||
<string name="warning_demo_mode_message">You are currently in the Demo mode</string>
|
||||
<string name="warning_demo_mode_title">Demo mode active</string>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.BoundsTransform
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.SharedTransitionLayout
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -7,6 +11,13 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import androidx.compose.ui.layout.Placeable
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
|
||||
@Composable
|
||||
fun TangemSharedTransitionLayout(
|
||||
|
|
@ -31,6 +42,78 @@ fun ProvideSharedTransitionScope(modifier: Modifier = Modifier, content: @Compos
|
|||
}
|
||||
}
|
||||
|
||||
private val LocalSharedTransitionScope = staticCompositionLocalOf<SharedTransitionScope> {
|
||||
error("No SharedTransitionScope provided")
|
||||
private val LocalSharedTransitionScope = staticCompositionLocalOf<SharedTransitionScope> { StubSharedTransitionScope() }
|
||||
|
||||
private class StubSharedTransitionScope : SharedTransitionScope {
|
||||
override val isTransitionActive: Boolean = false
|
||||
|
||||
override val Placeable.PlacementScope.lookaheadScopeCoordinates: LayoutCoordinates
|
||||
get() = error("No LookaheadScopeCoordinates available in StubSharedTransitionScope")
|
||||
|
||||
override fun Modifier.skipToLookaheadSize(enabled: () -> Boolean): Modifier {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun Modifier.renderInSharedTransitionScopeOverlay(
|
||||
zIndexInOverlay: Float,
|
||||
renderInOverlay: () -> Boolean,
|
||||
): Modifier {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun Modifier.sharedElement(
|
||||
sharedContentState: SharedTransitionScope.SharedContentState,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope,
|
||||
boundsTransform: BoundsTransform,
|
||||
placeholderSize: SharedTransitionScope.PlaceholderSize,
|
||||
renderInOverlayDuringTransition: Boolean,
|
||||
zIndexInOverlay: Float,
|
||||
clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip,
|
||||
): Modifier {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun Modifier.sharedBounds(
|
||||
sharedContentState: SharedTransitionScope.SharedContentState,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope,
|
||||
enter: EnterTransition,
|
||||
exit: ExitTransition,
|
||||
boundsTransform: BoundsTransform,
|
||||
resizeMode: SharedTransitionScope.ResizeMode,
|
||||
placeholderSize: SharedTransitionScope.PlaceholderSize,
|
||||
renderInOverlayDuringTransition: Boolean,
|
||||
zIndexInOverlay: Float,
|
||||
clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip,
|
||||
): Modifier {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun Modifier.sharedElementWithCallerManagedVisibility(
|
||||
sharedContentState: SharedTransitionScope.SharedContentState,
|
||||
visible: Boolean,
|
||||
boundsTransform: BoundsTransform,
|
||||
placeholderSize: SharedTransitionScope.PlaceholderSize,
|
||||
renderInOverlayDuringTransition: Boolean,
|
||||
zIndexInOverlay: Float,
|
||||
clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip,
|
||||
): Modifier {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun OverlayClip(clipShape: Shape): SharedTransitionScope.OverlayClip {
|
||||
return object : SharedTransitionScope.OverlayClip {
|
||||
override fun getClipPath(
|
||||
sharedContentState: SharedTransitionScope.SharedContentState,
|
||||
bounds: Rect,
|
||||
layoutDirection: LayoutDirection,
|
||||
density: Density,
|
||||
): Path? {
|
||||
return Path()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun LayoutCoordinates.toLookaheadCoordinates(): LayoutCoordinates {
|
||||
return this
|
||||
}
|
||||
}
|
||||
BIN
core/ui/src/main/res/drawable/bg_trending_article.webp
Normal file
BIN
core/ui/src/main/res/drawable/bg_trending_article.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,8 @@ import com.tangem.datasource.api.tangemTech.models.orDefault
|
|||
import com.tangem.datasource.utils.getSyncOrNull
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -77,6 +79,10 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
|||
return migratedResponse
|
||||
}
|
||||
|
||||
override fun get(userWalletId: UserWalletId): Flow<GetWalletAccountsResponse> {
|
||||
return getAccountsResponseStore(userWalletId = userWalletId).data.filterNotNull()
|
||||
}
|
||||
|
||||
override suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse? {
|
||||
return getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.data.account.converter.AccountNameConverter
|
|||
import com.tangem.data.account.converter.toDerivationIndex
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.utils.assignTokens
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
|
|
@ -22,6 +23,10 @@ import com.tangem.domain.account.tokens.MainAccountTokensMigration
|
|||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
|
|
@ -37,8 +42,12 @@ internal class DefaultMainAccountTokensMigration(
|
|||
private val accountsResponseStoreFactory: AccountsResponseStoreFactory,
|
||||
private val accountTokenMigrationStore: AccountTokenMigrationStore,
|
||||
private val userTokensSaver: UserTokensSaver,
|
||||
private val eTagsStore: ETagsStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
) : MainAccountTokensMigration {
|
||||
|
||||
private val coroutineScope = CoroutineScope(dispatchers.default + SupervisorJob())
|
||||
|
||||
internal suspend fun migrate(userWalletId: UserWalletId): Either<Throwable, GetWalletAccountsResponse> = either {
|
||||
val store = accountsResponseStoreFactory.create(userWalletId)
|
||||
val response = store.getSyncOrNull()
|
||||
|
|
@ -89,16 +98,7 @@ internal class DefaultMainAccountTokensMigration(
|
|||
|
||||
store.updateData { updatedResponse }
|
||||
|
||||
val userTokensResponse = updatedResponse.toUserTokensResponse()
|
||||
userTokensSaver.pushWithRetryer(
|
||||
userWalletId = userWalletId,
|
||||
response = userTokensResponse,
|
||||
onFailSend = {
|
||||
val exception = IllegalStateException("Failed to push updated tokens after migration")
|
||||
Timber.e(exception)
|
||||
raise(exception)
|
||||
},
|
||||
)
|
||||
pushTokens(userWalletId, updatedResponse)
|
||||
return@either updatedResponse
|
||||
}
|
||||
|
||||
|
|
@ -151,16 +151,7 @@ internal class DefaultMainAccountTokensMigration(
|
|||
val selectedAccountName = AccountNameConverter.convertBack(selectedAccount.name)
|
||||
accountTokenMigrationStore.store(userWalletId, mainAccountName to selectedAccountName)
|
||||
|
||||
val userTokensResponse = updatedResponse.toUserTokensResponse()
|
||||
userTokensSaver.pushWithRetryer(
|
||||
userWalletId = userWalletId,
|
||||
response = userTokensResponse,
|
||||
onFailSend = {
|
||||
val exception = IllegalStateException("Failed to push updated tokens after migration")
|
||||
Timber.e(exception)
|
||||
raise(exception)
|
||||
},
|
||||
)
|
||||
pushTokens(userWalletId, updatedResponse)
|
||||
}
|
||||
|
||||
private fun Raise<Throwable>.findAccount(
|
||||
|
|
@ -232,4 +223,20 @@ internal class DefaultMainAccountTokensMigration(
|
|||
accountNodeValue == derivationIndex.value.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Raise<Throwable>.pushTokens(userWalletId: UserWalletId, response: GetWalletAccountsResponse) {
|
||||
val userTokensResponse = response.toUserTokensResponse()
|
||||
userTokensSaver.pushWithRetryer(
|
||||
userWalletId = userWalletId,
|
||||
response = userTokensResponse,
|
||||
onFailSend = {
|
||||
coroutineScope.launch {
|
||||
eTagsStore.clear(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts)
|
||||
}
|
||||
val exception = IllegalStateException("Failed to push updated tokens after migration")
|
||||
Timber.e(exception)
|
||||
raise(exception)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.data.account.converter.createWalletAccountDTO
|
|||
import com.tangem.data.account.store.AccountsResponseStore
|
||||
import com.tangem.data.account.store.AccountsResponseStoreFactory
|
||||
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
|
||||
import com.tangem.data.common.cache.etag.ETagsStore
|
||||
import com.tangem.data.common.currency.UserTokensSaver
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
|
|
@ -17,6 +18,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.test.core.assertEither
|
||||
import com.tangem.test.core.assertEitherLeft
|
||||
import com.tangem.test.core.assertEitherRight
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
|
@ -34,11 +36,14 @@ class DefaultMainAccountTokensMigrationTest {
|
|||
private val accountsResponseStoreFlow = MutableStateFlow<GetWalletAccountsResponse?>(value = null)
|
||||
private val accountTokenMigrationStore = mockk<AccountTokenMigrationStore>(relaxUnitFun = true)
|
||||
private val userTokensSaver = mockk<UserTokensSaver>(relaxUnitFun = true)
|
||||
private val eTagsStore = mockk<ETagsStore>(relaxUnitFun = true)
|
||||
|
||||
private val migration = DefaultMainAccountTokensMigration(
|
||||
accountsResponseStoreFactory = accountsResponseStoreFactory,
|
||||
accountTokenMigrationStore = accountTokenMigrationStore,
|
||||
userTokensSaver = userTokensSaver,
|
||||
eTagsStore = eTagsStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("011")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.common.account
|
|||
|
||||
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Component for fetching wallet accounts
|
||||
|
|
@ -14,6 +15,9 @@ interface WalletAccountsFetcher {
|
|||
@Throws
|
||||
suspend fun fetch(userWalletId: UserWalletId): GetWalletAccountsResponse
|
||||
|
||||
/** Get wallet accounts by [userWalletId] */
|
||||
fun get(userWalletId: UserWalletId): Flow<GetWalletAccountsResponse>
|
||||
|
||||
/** Get saved wallet accounts by [userWalletId] */
|
||||
suspend fun getSaved(userWalletId: UserWalletId): GetWalletAccountsResponse?
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -7,16 +7,19 @@ import com.tangem.blockchainsdk.utils.OLD_POLYGON_NAME
|
|||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
|
||||
import com.tangem.data.common.account.WalletAccountsFetcher
|
||||
import com.tangem.data.onramp.converters.HotCryptoCurrencyConverter
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse
|
||||
import com.tangem.datasource.appcurrency.AppCurrencyResponseStore
|
||||
import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
import com.tangem.domain.card.common.extensions.canHandleBlockchain
|
||||
import com.tangem.domain.card.common.extensions.canHandleToken
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -54,7 +57,9 @@ internal class DefaultHotCryptoRepository(
|
|||
private val userWalletsStore: UserWalletsStore,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val appCurrencyResponseStore: AppCurrencyResponseStore,
|
||||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
private val userTokensResponseStore: UserTokensResponseStore,
|
||||
private val walletAccountsFetcher: WalletAccountsFetcher,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : HotCryptoRepository {
|
||||
|
|
@ -100,7 +105,11 @@ internal class DefaultHotCryptoRepository(
|
|||
private fun getWalletsWithTokensFlow(): Flow<Map<UserWallet, List<UserTokensResponse.Token>>> {
|
||||
return userWalletsStore.userWallets.flatMapLatest { userWallets ->
|
||||
val flows = userWallets.map { userWallet ->
|
||||
userTokensResponseStore.get(userWalletId = userWallet.walletId)
|
||||
if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
walletAccountsFetcher.get(userWalletId = userWallet.walletId).map { it.toUserTokensResponse() }
|
||||
} else {
|
||||
userTokensResponseStore.get(userWalletId = userWallet.walletId)
|
||||
}
|
||||
.map { userWallet to it?.tokens.orEmpty() }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -31,4 +31,5 @@ enum class PromoId {
|
|||
VisaPresale,
|
||||
BlackFriday,
|
||||
OnePlusOne,
|
||||
YieldPromo,
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ class ShouldShowPromoWalletUseCase(
|
|||
PromoId.VisaPresale,
|
||||
PromoId.BlackFriday,
|
||||
PromoId.OnePlusOne,
|
||||
PromoId.YieldPromo,
|
||||
-> true
|
||||
PromoId.Sepa -> {
|
||||
val walletFirstUsageDate = settingsRepository.getWalletFirstUsageDate()
|
||||
|
|
|
|||
|
|
@ -60,5 +60,6 @@ sealed class PromoAnalyticsEvent(
|
|||
Sepa("Sepa"),
|
||||
BlackFriday("Black Friday"),
|
||||
OnePlusOne("One-Plus-One"),
|
||||
YieldPromo("Yield Promo"),
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.tangem.domain.transaction.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.blockchain.common.MessageSigner
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.transaction.error.SignCloreMessageError
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
||||
class SignCloreMessageUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val getHotWalletSigner: (UserWallet.Hot) -> TransactionSigner,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
userWallet: UserWallet,
|
||||
currency: CryptoCurrency,
|
||||
message: String,
|
||||
): Either<SignCloreMessageError, String> {
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = currency.network,
|
||||
)
|
||||
|
||||
if (walletManager == null) {
|
||||
return SignCloreMessageError.WalletManagerNotFound.left()
|
||||
}
|
||||
|
||||
if (walletManager !is MessageSigner) {
|
||||
return SignCloreMessageError.MessageSigningNotSupported.left()
|
||||
}
|
||||
|
||||
val signer = when (userWallet) {
|
||||
is UserWallet.Cold -> {
|
||||
val card = userWallet.scanResponse.card
|
||||
val isCardNotBackedUp = card.backupStatus?.isActive != true
|
||||
cardSdkConfigRepository.getCommonSigner(
|
||||
cardId = card.cardId.takeIf { isCardNotBackedUp },
|
||||
twinKey = null,
|
||||
)
|
||||
}
|
||||
is UserWallet.Hot -> getHotWalletSigner(userWallet)
|
||||
}
|
||||
|
||||
return when (val result = walletManager.signMessage(message, signer)) {
|
||||
is CompletionResult.Success -> result.data.right()
|
||||
is CompletionResult.Failure -> SignCloreMessageError.SigningFailed(
|
||||
message = result.error.message ?: "Unknown error",
|
||||
).left()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ package com.tangem.domain.wallets.usecase
|
|||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.right
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.error.SaveWalletError
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.analytics.Settings
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
|
||||
/**
|
||||
|
|
@ -16,13 +19,20 @@ import com.tangem.domain.wallets.repository.WalletsRepository
|
|||
class SaveWalletUseCase(
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val walletsRepository: WalletsRepository,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either<SaveWalletError, Unit> {
|
||||
suspend operator fun invoke(
|
||||
userWallet: UserWallet,
|
||||
canOverride: Boolean = false,
|
||||
analyticsSource: AnalyticsParam.ScreensSources? = null,
|
||||
): Either<SaveWalletError, Unit> {
|
||||
return either {
|
||||
val newUserWallet =
|
||||
userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId }
|
||||
val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride).bind()
|
||||
val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride)
|
||||
.onRight { trackColdWalletAddedIfNeeded(analyticsSource, it) }
|
||||
.bind()
|
||||
|
||||
if (newUserWallet) {
|
||||
when (userWallet) {
|
||||
|
|
@ -50,4 +60,11 @@ class SaveWalletUseCase(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun trackColdWalletAddedIfNeeded(source: AnalyticsParam.ScreensSources?, userWallet: UserWallet) {
|
||||
val hasHotWallet = userWalletsListRepository.userWalletsSync().any { it is UserWallet.Hot }
|
||||
if (hasHotWallet && userWallet is UserWallet.Cold) {
|
||||
analyticsEventHandler.send(event = Settings.ColdWalletAdded(source))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.features.details.entity
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -11,6 +10,5 @@ internal data class UserWalletListUM(
|
|||
val userWallets: ImmutableList<UserWalletItemUM>,
|
||||
val isWalletSavingInProgress: Boolean,
|
||||
val addNewWalletText: TextReference,
|
||||
@DrawableRes val addNewWalletIconRes: Int?,
|
||||
val onAddNewWalletClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -55,7 +55,6 @@ internal class UserWalletListModel @Inject constructor(
|
|||
isWalletSavingInProgress = false,
|
||||
addNewWalletText = TextReference.EMPTY,
|
||||
onAddNewWalletClick = ::onAddNewWalletClick,
|
||||
addNewWalletIconRes = R.drawable.ic_plus_24,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,12 @@ internal class UserWalletSaver @Inject constructor(
|
|||
|
||||
private suspend fun Raise<Error>.saveWallet(userWallet: UserWallet) {
|
||||
fold(
|
||||
block = { saveWalletUseCase(userWallet).bind() },
|
||||
block = {
|
||||
saveWalletUseCase(
|
||||
userWallet = userWallet,
|
||||
analyticsSource = AnalyticsParam.ScreensSources.Settings,
|
||||
).bind()
|
||||
},
|
||||
recover = { error ->
|
||||
when (error) {
|
||||
is SaveWalletError.WalletAlreadySaved -> {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@ import kotlinx.serialization.Serializable
|
|||
data class AvailableToAddData(
|
||||
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet>,
|
||||
) {
|
||||
val isAvailableToAdd: Boolean
|
||||
get() = availableToAddWallets.isNotEmpty()
|
||||
val isAvailableToAdd: Boolean = availableToAddWallets.values.any { item -> item.isAvailableToAdd }
|
||||
val isSinglePortfolio: Boolean
|
||||
get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1
|
||||
}
|
||||
|
|
@ -23,7 +22,9 @@ data class AvailableToAddWallet(
|
|||
val accounts: List<AccountStatus>,
|
||||
val availableNetworks: Set<TokenMarketInfo.Network>,
|
||||
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount>,
|
||||
)
|
||||
) {
|
||||
val isAvailableToAdd: Boolean = availableToAddAccounts.values.any { item -> item.isAvailableToAdd }
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AvailableToAddAccount(
|
||||
|
|
@ -38,6 +39,8 @@ data class AvailableToAddAccount(
|
|||
.filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } }
|
||||
.toSet()
|
||||
|
||||
val isAvailableToAdd: Boolean = availableToAddNetworks.isNotEmpty()
|
||||
|
||||
val addedMarketNetworks: Set<TokenMarketInfo.Network> = availableNetworks
|
||||
.filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } }
|
||||
.toSet()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@ import kotlinx.serialization.Serializable
|
|||
internal data class AvailableToAddData(
|
||||
val availableToAddWallets: Map<UserWalletId, AvailableToAddWallet>,
|
||||
) {
|
||||
val isAvailableToAdd: Boolean
|
||||
get() = availableToAddWallets.isNotEmpty()
|
||||
val isAvailableToAdd: Boolean = availableToAddWallets.values.any { item -> item.isAvailableToAdd }
|
||||
val isSinglePortfolio: Boolean
|
||||
get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1
|
||||
}
|
||||
|
|
@ -23,7 +22,9 @@ internal data class AvailableToAddWallet(
|
|||
val accounts: List<AccountStatus>,
|
||||
val availableNetworks: Set<TokenMarketInfo.Network>,
|
||||
val availableToAddAccounts: Map<AccountId, AvailableToAddAccount>,
|
||||
)
|
||||
) {
|
||||
val isAvailableToAdd: Boolean = availableToAddAccounts.values.any { item -> item.isAvailableToAdd }
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class AvailableToAddAccount(
|
||||
|
|
@ -31,6 +32,7 @@ internal data class AvailableToAddAccount(
|
|||
val availableNetworks: Set<TokenMarketInfo.Network>,
|
||||
val addedNetworks: Set<Network>,
|
||||
) {
|
||||
|
||||
val isSingleNetwork: Boolean
|
||||
get() = availableNetworks.size == 1
|
||||
|
||||
|
|
@ -38,6 +40,8 @@ internal data class AvailableToAddAccount(
|
|||
.filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } }
|
||||
.toSet()
|
||||
|
||||
val isAvailableToAdd: Boolean = availableToAddNetworks.isNotEmpty()
|
||||
|
||||
val addedMarketNetworks: Set<TokenMarketInfo.Network> = availableNetworks
|
||||
.filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } }
|
||||
.toSet()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ internal object SwapAmountQuoteUtils {
|
|||
}
|
||||
|
||||
return copy(
|
||||
isPrimaryButtonEnabled = updatedAmountField.amountField.isPrimaryButtonEnabled,
|
||||
primaryAmount = updatedAmountField,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
|||
val swapDirection: SwapDirection,
|
||||
val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||
val primaryCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
|
||||
val primaryFeePaidCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>,
|
||||
val primaryFeePaidCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>, // doesn't change if select gasless fee
|
||||
val accountFlow: StateFlow<Account.CryptoPortfolio?>,
|
||||
val isAccountModeFlow: StateFlow<Boolean>,
|
||||
val callback: ModelCallback,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ import com.tangem.feature.swap.domain.models.SwapAmount
|
|||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import com.tangem.feature.swap.models.AddToPortfolioRoute
|
||||
import com.tangem.feature.swap.models.SwapCardState
|
||||
import com.tangem.feature.swap.models.SwapStateHolder
|
||||
import com.tangem.feature.swap.models.UiActions
|
||||
import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager
|
||||
|
|
@ -200,7 +199,12 @@ internal class SwapModel @Inject constructor(
|
|||
private val searchDebouncer = Debouncer()
|
||||
private val singleTaskScheduler = SingleTaskScheduler<Map<SwapProvider, SwapState>>()
|
||||
|
||||
var dataState by mutableStateOf(SwapProcessDataState())
|
||||
val dataStateStateFlow = MutableStateFlow(SwapProcessDataState())
|
||||
var dataState
|
||||
get() = dataStateStateFlow.value
|
||||
set(value) {
|
||||
dataStateStateFlow.value = value
|
||||
}
|
||||
|
||||
var uiState: SwapStateHolder by mutableStateOf(
|
||||
stateBuilder.createInitialLoadingState(
|
||||
|
|
@ -220,9 +224,17 @@ internal class SwapModel @Inject constructor(
|
|||
private val swapRouter: SwapRouter = SwapRouter(router = router)
|
||||
private var userCountry: UserCountry? = null
|
||||
|
||||
private lateinit var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus
|
||||
private var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null
|
||||
private var toAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null
|
||||
|
||||
/**
|
||||
* If accountsFeatureToggles is off OR user came from Tangem Pay -> fromAccountCurrencyStatus == null
|
||||
* If accountsFeatureToggles is on AND user didn't come from Tangem Pay -> fromAccountCurrencyStatus != null
|
||||
*
|
||||
* Remove when accounts are integrated into Tangem Pay
|
||||
*/
|
||||
private val canUseFromAccountCurrencyStatus = accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null
|
||||
|
||||
private val isUserResolvableError: (SwapState) -> Boolean = { swapState ->
|
||||
swapState is SwapState.SwapError &&
|
||||
(
|
||||
|
|
@ -288,7 +300,7 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
|
||||
modelScope.launch(dispatchers.io) {
|
||||
if (accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null) {
|
||||
if (canUseFromAccountCurrencyStatus) {
|
||||
isAccountsMode = isAccountsModeEnabledUseCase.invokeSync()
|
||||
|
||||
val fromAccountStatus = getAccountCurrencyStatusUseCase.invokeSync(
|
||||
|
|
@ -579,11 +591,11 @@ internal class SwapModel @Inject constructor(
|
|||
} else {
|
||||
initialFromStatus to selectedCurrency
|
||||
}
|
||||
val (fromAccount, toAccount) = if (accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null) {
|
||||
val (fromAccount, toAccount) = if (canUseFromAccountCurrencyStatus) {
|
||||
if (isOrderReversed) {
|
||||
selectedAccount to fromAccountCurrencyStatus.account
|
||||
selectedAccount to requireNotNull(fromAccountCurrencyStatus).account
|
||||
} else {
|
||||
fromAccountCurrencyStatus.account to selectedAccount
|
||||
requireNotNull(fromAccountCurrencyStatus).account to selectedAccount
|
||||
}
|
||||
} else {
|
||||
null to null
|
||||
|
|
@ -691,6 +703,7 @@ internal class SwapModel @Inject constructor(
|
|||
toProvidersList: List<SwapProvider>,
|
||||
updateFeeBlock: Boolean = true,
|
||||
): PeriodicTask<Map<SwapProvider, SwapState>> {
|
||||
var shouldUpdateFeeBlock = updateFeeBlock
|
||||
return PeriodicTask(
|
||||
delay = UPDATE_DELAY,
|
||||
task = {
|
||||
|
|
@ -726,8 +739,10 @@ internal class SwapModel @Inject constructor(
|
|||
tokenSwapInfoForProviders = successStates.entries
|
||||
.associate { it.key.providerId to it.value.toTokenInfo },
|
||||
)
|
||||
if (updateFeeBlock) {
|
||||
if (shouldUpdateFeeBlock) {
|
||||
modelScope.launch { feeSelectorReloadTrigger.triggerUpdate() }
|
||||
} else {
|
||||
shouldUpdateFeeBlock = true
|
||||
}
|
||||
} else {
|
||||
feeSelectorRepository.state.value = FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true)
|
||||
|
|
@ -1266,7 +1281,7 @@ internal class SwapModel @Inject constructor(
|
|||
fromAccount = foundAccount
|
||||
toToken = initialFromStatus
|
||||
toAccount = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
fromAccountCurrencyStatus.account
|
||||
fromAccountCurrencyStatus?.account
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -1284,7 +1299,7 @@ internal class SwapModel @Inject constructor(
|
|||
} else {
|
||||
fromToken = initialFromStatus
|
||||
fromAccount = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
fromAccountCurrencyStatus.account
|
||||
fromAccountCurrencyStatus?.account
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -2132,20 +2147,19 @@ internal class SwapModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("UnsafeCallOnNullableType")
|
||||
inner class FeeSelectorRepository : SwapFeeSelectorBlockComponent.ModelRepositoryExtended {
|
||||
|
||||
override val state = MutableStateFlow<FeeSelectorUM>(FeeSelectorUM.Loading)
|
||||
override val state = MutableStateFlow<FeeSelectorUM>(
|
||||
FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true),
|
||||
)
|
||||
|
||||
override suspend fun loadFeeExtended(
|
||||
selectedToken: CryptoCurrencyStatus?,
|
||||
): Either<GetFeeError, TransactionFeeExtended> {
|
||||
val sendCardData =
|
||||
uiState.sendCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val receiveCardData =
|
||||
uiState.receiveCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val fromToken = sendCardData.token ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val toToken = receiveCardData.token ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val selectedProvider = dataState.selectedProvider ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val fromToken = dataState.fromCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val toToken = dataState.toCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val selectedProvider = dataStateStateFlow.first { it.selectedProvider != null }.selectedProvider!!
|
||||
|
||||
if (dataState.lastLoadedSwapStates[selectedProvider] !is SwapState.QuotesLoadedState) {
|
||||
return Either.Left(GetFeeError.UnknownError)
|
||||
|
|
@ -2201,13 +2215,9 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
|
||||
val sendCardData =
|
||||
uiState.sendCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val receiveCardData =
|
||||
uiState.receiveCardData as? SwapCardState.SwapCardData ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val fromToken = sendCardData.token ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val toToken = receiveCardData.token ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val selectedProvider = dataState.selectedProvider ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val fromToken = dataState.fromCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val toToken = dataState.toCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError)
|
||||
val selectedProvider = dataStateStateFlow.first { it.selectedProvider != null }.selectedProvider!!
|
||||
|
||||
if (dataState.lastLoadedSwapStates[selectedProvider] !is SwapState.QuotesLoadedState) {
|
||||
return Either.Left(GetFeeError.UnknownError)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
-> {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = "",
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -152,6 +152,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
|
||||
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||
private val yieldSupplyGetRewardsBalanceUseCase: YieldSupplyGetRewardsBalanceUseCase,
|
||||
private val signCloreMessageUseCase: SignCloreMessageUseCase,
|
||||
) : Model(),
|
||||
TokenDetailsClickIntents,
|
||||
ExpressTransactionsClickIntents,
|
||||
|
|
@ -161,7 +162,8 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
private val userWalletId: UserWalletId = params.userWalletId
|
||||
private val cryptoCurrency: CryptoCurrency = params.currency
|
||||
|
||||
private val userWallet: UserWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: error("UserWallet not found")
|
||||
private val userWallet: UserWallet = getUserWalletUseCase(userWalletId).getOrNull()
|
||||
?: error("UserWallet not found")
|
||||
|
||||
private val marketPriceJobHolder = JobHolder()
|
||||
private val refreshStateJobHolder = JobHolder()
|
||||
|
|
@ -194,6 +196,27 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
yieldSupplyFeatureToggles = yieldSupplyFeatureToggles,
|
||||
)
|
||||
|
||||
private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency))
|
||||
val uiState: StateFlow<TokenDetailsState> = internalUiState
|
||||
|
||||
// region Clore migration
|
||||
// TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY])
|
||||
private val cloreMigrationModel by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
CloreMigrationModel(
|
||||
stateFactory = stateFactory,
|
||||
signCloreMessageUseCase = signCloreMessageUseCase,
|
||||
clipboardManager = clipboardManager,
|
||||
uiMessageSender = uiMessageSender,
|
||||
router = router,
|
||||
userWallet = userWallet,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
coroutineScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
onStateUpdate = { internalUiState.value = it },
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
||||
private val expressStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
expressStatusFactory.create(
|
||||
clickIntents = this,
|
||||
|
|
@ -216,9 +239,6 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
TokenDetailsCurrencyStatusAnalyticsSender(analyticsEventsHandler)
|
||||
}
|
||||
|
||||
private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency))
|
||||
val uiState: StateFlow<TokenDetailsState> = internalUiState
|
||||
|
||||
init {
|
||||
updateTopBarMenu()
|
||||
initButtons()
|
||||
|
|
@ -667,9 +687,8 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click)
|
||||
clipboardManager.setText(text = extendedKey, isSensitive = true)
|
||||
|
||||
uiMessageSender.send(
|
||||
message = SnackbarMessage(message = resourceReference(R.string.wallet_notification_address_copied)),
|
||||
)
|
||||
val message = resourceReference(R.string.wallet_notification_address_copied)
|
||||
uiMessageSender.send(message = SnackbarMessage(message = message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -786,7 +805,8 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
modelScope.launch(dispatchers.main) {
|
||||
when (val addresses = currencyStatus.value.networkAddress) {
|
||||
is NetworkAddress.Selectable -> {
|
||||
internalUiState.value = stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses)
|
||||
internalUiState.value =
|
||||
stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses)
|
||||
}
|
||||
is NetworkAddress.Single -> {
|
||||
router.openUrl(
|
||||
|
|
@ -1256,11 +1276,26 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleNavigationParam() {
|
||||
if (params.navigationAction is NavigationAction.Staking) {
|
||||
openStaking()
|
||||
when (params.navigationAction) {
|
||||
is NavigationAction.Staking -> openStaking()
|
||||
is NavigationAction.CloreMigration -> onCloreMigrationClick()
|
||||
is NavigationAction.YieldSupply,
|
||||
null,
|
||||
-> Unit
|
||||
}
|
||||
}
|
||||
|
||||
// region Clore migration
|
||||
// TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY])
|
||||
|
||||
override fun onCloreMigrationClick() = cloreMigrationModel.onCloreMigrationClick()
|
||||
|
||||
override fun onCloreSignMessage(message: String) = cloreMigrationModel.onCloreSignMessage(message)
|
||||
|
||||
override fun onOpenCloreClaimPortal() = cloreMigrationModel.onOpenCloreClaimPortal()
|
||||
|
||||
// endregion Clore migration
|
||||
|
||||
private companion object {
|
||||
const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,220 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R as CoreR
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.fields.SimpleTextField
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun CloreMigrationBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemModalBottomSheet<CloreMigrationBottomSheetConfig>(
|
||||
config = config,
|
||||
title = {
|
||||
TangemModalBottomSheetTitle(
|
||||
endIconRes = CoreR.drawable.ic_close_24,
|
||||
onEndClick = config.onDismissRequest,
|
||||
)
|
||||
},
|
||||
) { content: CloreMigrationBottomSheetConfig ->
|
||||
CloreMigrationBottomSheetContent(content = content)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CloreMigrationBottomSheetContent(content: CloreMigrationBottomSheetConfig) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.warning_clore_migration_sheet_title),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.warning_clore_migration_sheet_description),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
MessageField(
|
||||
placeholder = stringResourceSafe(R.string.warning_clore_migration_message_label),
|
||||
value = content.message,
|
||||
onValueChange = content.onMessageChange,
|
||||
buttonText = stringResourceSafe(R.string.warning_clore_migration_sign_button),
|
||||
onButtonClick = content.onSignClick,
|
||||
isButtonEnabled = content.message.isNotBlank() && !content.isSigningInProgress,
|
||||
isLoading = content.isSigningInProgress,
|
||||
)
|
||||
|
||||
SignatureField(
|
||||
placeholder = stringResourceSafe(R.string.warning_clore_migration_signature_label),
|
||||
value = content.signature,
|
||||
onCopyClick = content.onCopyClick,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
PrimaryButton(
|
||||
text = stringResourceSafe(R.string.warning_clore_migration_open_portal_button),
|
||||
onClick = content.onOpenPortalClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun MessageField(
|
||||
placeholder: String,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
buttonText: String,
|
||||
onButtonClick: () -> Unit,
|
||||
isButtonEnabled: Boolean,
|
||||
isLoading: Boolean,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersSmall,
|
||||
)
|
||||
.heightIn(min = 56.dp)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
placeholder = TextReference.Str(placeholder),
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = false,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = TextReference.Str(buttonText),
|
||||
onClick = onButtonClick,
|
||||
isEnabled = isButtonEnabled,
|
||||
isLoading = isLoading,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("FunctionSignature")
|
||||
@Composable
|
||||
private fun SignatureField(placeholder: String, value: String, onCopyClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersSmall,
|
||||
)
|
||||
.heightIn(min = 56.dp)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
onValueChange = {},
|
||||
placeholder = TextReference.Str(placeholder),
|
||||
modifier = Modifier.weight(1f),
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = TextReference.Res(R.string.warning_clore_migration_copy_button),
|
||||
onClick = onCopyClick,
|
||||
isEnabled = value.isNotBlank(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_CloreMigrationBottomSheet() {
|
||||
TangemThemePreview {
|
||||
val config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
content = CloreMigrationBottomSheetConfig(
|
||||
message = "Claim request for CLORE tokens to Ethereum address " +
|
||||
"0x742d35Cc6634C0532925a3b844Bc9e7595f60126 from CEsMERNgBVPo9Dkc99pRDMPD3mxwPMxHZi",
|
||||
signature = "H4sIAAAAAAAEAGNgGAWjYBSMglEwCkYBHQEA",
|
||||
isSigningInProgress = false,
|
||||
onMessageChange = {},
|
||||
onSignClick = {},
|
||||
onCopyClick = {},
|
||||
onOpenPortalClick = {},
|
||||
),
|
||||
onDismissRequest = {},
|
||||
)
|
||||
|
||||
CloreMigrationBottomSheet(config)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
private fun Preview_CloreMigrationBottomSheet_Empty() {
|
||||
TangemThemePreview {
|
||||
val config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
content = CloreMigrationBottomSheetConfig(
|
||||
message = "",
|
||||
signature = "",
|
||||
isSigningInProgress = false,
|
||||
onMessageChange = {},
|
||||
onSignClick = {},
|
||||
onCopyClick = {},
|
||||
onOpenPortalClick = {},
|
||||
),
|
||||
onDismissRequest = {},
|
||||
)
|
||||
|
||||
CloreMigrationBottomSheet(config)
|
||||
}
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
|||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
|
|
@ -35,6 +36,7 @@ import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
|||
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent
|
||||
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program
|
||||
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked
|
||||
import com.tangem.domain.tokens.model.details.NavigationAction
|
||||
import com.tangem.domain.wallets.usecase.*
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
|
||||
|
|
@ -60,6 +62,8 @@ internal interface WalletWarningsClickIntents {
|
|||
|
||||
fun onAddBackupCardClick()
|
||||
|
||||
fun onCloreMigrationClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
|
||||
|
||||
fun onCloseAlreadySignedHashesWarningClick()
|
||||
|
||||
fun onGenerateMissedAddressesClick(missedAddressCurrencies: List<CryptoCurrency>)
|
||||
|
|
@ -95,6 +99,8 @@ internal interface WalletWarningsClickIntents {
|
|||
fun onDenyPermissions()
|
||||
|
||||
fun onFinishWalletActivationClick(isBackupExists: Boolean)
|
||||
|
||||
fun onYieldPromoTermsAndConditionsClick()
|
||||
}
|
||||
|
||||
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
|
||||
|
|
@ -135,6 +141,16 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
prepareAndStartOnboardingProcess()
|
||||
}
|
||||
|
||||
override fun onCloreMigrationClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
|
||||
val userWallet = getSelectedUserWallet() ?: return
|
||||
|
||||
router.openTokenDetails(
|
||||
userWalletId = userWallet.walletId,
|
||||
currencyStatus = cryptoCurrencyStatus,
|
||||
navigationAction = NavigationAction.CloreMigration,
|
||||
)
|
||||
}
|
||||
|
||||
private fun prepareAndStartOnboardingProcess() {
|
||||
modelScope.launch(dispatchers.main) {
|
||||
getSelectedUserWallet()?.requireColdWallet()?.let {
|
||||
|
|
@ -271,6 +287,11 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
program = Program.OnePlusOne,
|
||||
action = PromotionBannerClicked.BannerAction.Closed(),
|
||||
)
|
||||
PromoId.YieldPromo -> PromotionBannerClicked(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
program = Program.YieldPromo,
|
||||
action = PromotionBannerClicked.BannerAction.Closed(),
|
||||
)
|
||||
},
|
||||
|
||||
)
|
||||
|
|
@ -328,6 +349,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
)
|
||||
urlOpener.openUrl(ONE_PLUS_ONE_PROMO_LINK)
|
||||
}
|
||||
PromoId.YieldPromo -> Unit // banner is not clickable, only terms and conditions button
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -545,6 +567,17 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onYieldPromoTermsAndConditionsClick() {
|
||||
analyticsEventHandler.send(
|
||||
PromotionBannerClicked(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
program = Program.YieldPromo,
|
||||
action = PromotionBannerClicked.BannerAction.Clicked(),
|
||||
),
|
||||
)
|
||||
urlOpener.openUrl(YIELD_PROMO_TERMS_LINK)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val VISA_PROMO_LINK = "https://tangem.com/en/cardwaitlist/?utm_source=tangem-app-banner" +
|
||||
"&utm_medium=banner" +
|
||||
|
|
@ -559,5 +592,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
"&utm_source=tangem-app-banner" +
|
||||
"&utm_medium=banner" +
|
||||
"&utm_campaign=BOGO50"
|
||||
const val YIELD_PROMO_TERMS_LINK = "https://tangem.com/docs/yield-mode-toc.html"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ sealed class WalletScreenAnalyticsEvent {
|
|||
}
|
||||
put("Wallet Type", seedPhrase)
|
||||
},
|
||||
)
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
data class NoticeFinishActivation(
|
||||
private val activationState: ActivationState,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import com.tangem.feature.wallet.impl.R
|
|||
import com.tangem.feature.wallet.presentation.account.AccountDependencies
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.extensions.addIf
|
||||
import com.tangem.utils.extensions.isPositive
|
||||
import com.tangem.utils.extensions.orZero
|
||||
|
|
@ -95,6 +96,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
notificationsRepository.getShouldShowNotification(NotificationId.EnablePushesReminderNotification.key)
|
||||
.distinctUntilChanged(),
|
||||
getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(),
|
||||
shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo)
|
||||
.distinctUntilChanged(),
|
||||
) { array -> array }
|
||||
.combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) }
|
||||
.map { array ->
|
||||
|
|
@ -107,6 +110,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
val shouldShowOnePlusOnePromo = array[4] as Boolean
|
||||
val shouldShowEnablePushesReminderNotification = array[5] as Boolean
|
||||
val shouldAccessCodeSkipped = array[6] as Boolean
|
||||
val shouldShowYieldPromo = array[7] as Boolean
|
||||
|
||||
buildList {
|
||||
addUsedOutdatedDataNotification(totalFiatBalance)
|
||||
|
|
@ -122,6 +126,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
|
||||
addOnePlusOnePromoNotification(clickIntents, shouldShowOnePlusOnePromo)
|
||||
|
||||
addYieldPromoNotification(clickIntents, shouldShowYieldPromo)
|
||||
|
||||
addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents)
|
||||
|
||||
addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents)
|
||||
|
|
@ -286,6 +292,19 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addYieldPromoNotification(
|
||||
clickIntents: WalletClickIntents,
|
||||
shouldShowPromo: Boolean,
|
||||
) {
|
||||
addIf(
|
||||
element = WalletNotification.YieldPromo(
|
||||
onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.YieldPromo) },
|
||||
onTermsAndConditionsClick = { clickIntents.onYieldPromoTermsAndConditionsClick() },
|
||||
),
|
||||
condition = shouldShowPromo,
|
||||
)
|
||||
}
|
||||
|
||||
// private fun MutableList<WalletNotification>.addYieldSupplyNotifications(
|
||||
// flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
|
||||
// ) {
|
||||
|
|
@ -317,6 +336,29 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
element = WalletNotification.Warning.SomeNetworksUnreachable,
|
||||
condition = flattenCurrencies.hasUnreachableNetworks(),
|
||||
)
|
||||
|
||||
addCloreMigrationNotification(flattenCurrencies, clickIntents)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addCloreMigrationNotification(
|
||||
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return
|
||||
|
||||
add(
|
||||
WalletNotification.CloreMigration(
|
||||
onStartMigrationClick = { clickIntents.onCloreMigrationClick(cloreCurrency) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun Lce<TokenListError, List<CryptoCurrencyStatus>>.findCloreCurrency(): CryptoCurrencyStatus? {
|
||||
val currencies = getOrNull(isPartialContentAccepted = true) ?: return null
|
||||
|
||||
return currencies.find { currencyStatus ->
|
||||
BlockchainUtils.isClore(currencyStatus.currency.network.rawId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addPushReminderNotification(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="36dp"
|
||||
android:viewportWidth="36"
|
||||
android:viewportHeight="36">
|
||||
<path
|
||||
android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
|
||||
android:strokeAlpha="0.1"
|
||||
android:fillColor="#0099FF"
|
||||
android:fillAlpha="0.1"/>
|
||||
<path
|
||||
android:pathData="M18.057,7.745C20.247,7.745 21.968,7.745 23.312,7.926C24.688,8.111 25.781,8.498 26.639,9.356C27.497,10.214 27.885,11.307 28.07,12.683C28.25,14.027 28.25,15.748 28.25,17.938V18.052C28.25,20.243 28.25,21.964 28.07,23.307C27.885,24.684 27.497,25.776 26.639,26.635C25.781,27.493 24.688,27.879 23.312,28.064C21.968,28.245 20.247,28.245 18.057,28.245H17.943C15.752,28.245 14.031,28.245 12.688,28.064C11.311,27.879 10.219,27.493 9.361,26.635C8.502,25.776 8.116,24.684 7.931,23.307C7.75,21.964 7.75,20.243 7.75,18.052V17.938C7.75,15.748 7.75,14.027 7.931,12.683C8.116,11.307 8.503,10.214 9.361,9.356C10.219,8.498 11.311,8.111 12.688,7.926C14.031,7.745 15.752,7.745 17.943,7.745H18.057ZM13,21.248C12.586,21.248 12.25,21.584 12.25,21.998V23.998C12.25,24.412 12.586,24.748 13,24.748C13.414,24.748 13.75,24.412 13.75,23.998V21.998C13.75,21.584 13.414,21.248 13,21.248ZM18,20.247C17.586,20.247 17.25,20.583 17.25,20.997V23.997C17.25,24.411 17.586,24.747 18,24.747C18.414,24.747 18.75,24.411 18.75,23.997V20.997C18.75,20.583 18.414,20.247 18,20.247ZM23,18.245C22.586,18.245 22.25,18.581 22.25,18.995V23.995C22.25,24.409 22.586,24.745 23,24.745C23.414,24.745 23.75,24.409 23.75,23.995V18.995C23.75,18.581 23.414,18.245 23,18.245ZM21.859,11.247C21.654,11.256 21.416,11.295 21.179,11.335C21.168,11.337 21.157,11.339 21.146,11.341L19.878,11.55C19.47,11.617 19.193,12.002 19.26,12.411C19.327,12.82 19.714,13.097 20.122,13.029L20.846,12.91C18.963,15.552 15.953,16.745 12,16.745C11.586,16.745 11.25,17.081 11.25,17.495C11.25,17.909 11.586,18.245 12,18.245C16.322,18.245 19.898,16.9 22.132,13.689L22.279,14.201C22.393,14.599 22.808,14.829 23.206,14.716C23.604,14.601 23.835,14.186 23.721,13.788L23.403,12.677C23.399,12.666 23.396,12.655 23.393,12.643C23.335,12.44 23.271,12.218 23.189,12.036C23.088,11.814 22.93,11.576 22.645,11.414C22.369,11.257 22.09,11.236 21.859,11.247Z"
|
||||
android:fillColor="#0099FF"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
# https://github.com/tangem/tangem-sdk-android/
|
||||
# https://github.com/tangem/vico
|
||||
|
||||
tangemBlockchainSdk = "develop-1391"
|
||||
tangemBlockchainSdk = "develop-1408"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "develop-573"
|
||||
tangemCardSdk = "develop-577"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
tangemVico = "2.0.0-alpha.25-tangem12"
|
||||
#tangemVico = "0.0.1" # Keep it! - used for local builds ^
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit dc9df17919126bce93482559fe26602f9801eb42
|
||||
Subproject commit 35755ad66f5f8fa6bcb1c84b443826cd315495d0
|
||||
Loading…
Add table
Add a link
Reference in a new issue