Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-19 14:58:45 +05:00
parent 990b06c3ed
commit 0ce75f1445
13 changed files with 154 additions and 2 deletions

View file

@ -140,6 +140,7 @@ internal class ChildFactory @Inject constructor(
AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS
AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES
AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT
AppRoute.ManageTokens.Source.TOKEN_SYNC_BANNER -> ManageTokensSource.TOKEN_SYNC_BANNER
}
val mode = route.accountId?.let { ManageTokensMode.Account(it) } ?: ManageTokensMode.None

View file

@ -139,6 +139,7 @@ sealed class AppRoute(val path: String) : Route {
STORIES,
SETTINGS,
ACCOUNT,
TOKEN_SYNC_BANNER,
}
}

View file

@ -140,6 +140,8 @@ object PreferencesKeys {
val HAS_HAD_FIRST_TOP_UP_KEY by lazy { stringPreferencesKey(name = "hasHadFirstTopUp") }
val PENDING_DISCOVERY_SYNC_KEY by lazy { stringPreferencesKey(name = "pendingDiscoverySync") }
// region Notifications
val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") }

View file

@ -692,6 +692,9 @@
<string name="initial_message_scan_header">Tap to scan</string>
<string name="initial_message_sign_header">Tap to sign</string>
<string name="initial_message_tap_header">Tap the card or ring</string>
<string name="initial_wallet_sync_banner_description">Your wallet is synced and ready.\nSome tokens missing?</string>
<string name="initial_wallet_sync_banner_title">Wallet successfully imported</string>
<string name="initial_wallet_sync_restore_progress">Restoring %d%</string>
<string name="key_invalidated_warning_description">You have updated biometrics, scan your card or ring to enter</string>
<string name="koinos_insufficient_balance_to_send_koin_description">Your balance should be higher than the fee value to make a transfer</string>
<string name="koinos_insufficient_balance_to_send_koin_title">Not enough balance</string>

View file

@ -0,0 +1,14 @@
package com.tangem.domain.tokens.model.tokensync
import java.math.BigDecimal
data class DiscoveredToken(
val contractAddress: String?,
val symbol: String,
val name: String,
val decimals: Int,
val amount: BigDecimal,
val isNativeToken: Boolean,
val currencyId: String?,
val networkId: String,
)

View file

@ -0,0 +1,22 @@
package com.tangem.domain.tokens.model.tokensync
sealed class TokenSyncProgress {
data object Idle : TokenSyncProgress()
data class InProgress(
val completedNetworks: Int,
val totalNetworks: Int,
) : TokenSyncProgress() {
val progressPercent: Int
get() = if (totalNetworks > 0) {
completedNetworks * 100 / totalNetworks
} else {
0
}
}
data object Completed : TokenSyncProgress()
data class Error(val cause: Throwable) : TokenSyncProgress()
}

View file

@ -0,0 +1,23 @@
package com.tangem.domain.tokens.repository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.model.tokensync.TokenSyncProgress
import kotlinx.coroutines.flow.Flow
interface TokenSyncRepository {
suspend fun runSync(userWalletId: UserWalletId)
suspend fun getPendingSyncWalletIds(): List<UserWalletId>
fun observeSyncProgress(userWalletId: UserWalletId): Flow<TokenSyncProgress>
fun acknowledgeCompletion(userWalletId: UserWalletId)
suspend fun clearPendingFlag(userWalletId: UserWalletId)
suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List<CryptoCurrency>
suspend fun clearDiscoveredTokens(userWalletId: UserWalletId)
}

View file

@ -9,6 +9,7 @@ enum class ManageTokensSource(val analyticsName: String) {
ONBOARDING(analyticsName = "Onboarding"),
SETTINGS(analyticsName = "Wallet Settings"),
ACCOUNT(analyticsName = "Account"),
TOKEN_SYNC_BANNER(analyticsName = "Token Sync Banner"),
SEND_VIA_SWAP(analyticsName = "SendViaSwap"),
}

View file

@ -122,6 +122,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.Warning.TangemPayRefreshNeeded -> null
is WalletNotification.Warning.TangemPayUnreachable -> null
is WalletNotification.UpgradeHotWalletPromo -> null
is WalletNotification.TokenSyncCompleted -> null
}
}

View file

@ -8,4 +8,5 @@ data class WalletAdditionalInfo(
val hideable: Boolean,
val content: TextReference,
val isHotBackedUp: Boolean = false,
val shouldShowProgress: Boolean = false,
)

View file

@ -463,4 +463,21 @@ sealed class WalletNotification(val config: NotificationConfig) {
iconSize = 72.dp,
),
)
data class TokenSyncCompleted(
val onCloseClick: () -> Unit,
val onManageTokensClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.initial_wallet_sync_banner_title),
subtitle = resourceReference(R.string.initial_wallet_sync_banner_description),
iconResId = R.drawable.ic_check_circle_24,
iconTint = IconTint.Accent,
onCloseClick = onCloseClick,
buttonsState = ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.main_manage_tokens),
onClick = onManageTokensClick,
),
),
)
}

View file

@ -0,0 +1,52 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
internal class SetTokenSyncProgressTransformer(
userWalletId: UserWalletId,
private val progressPercent: Int,
) : WalletStateTransformer(userWalletId) {
override fun transform(prevState: WalletState): WalletState {
return when (prevState) {
is WalletState.MultiCurrency.Content -> {
val updatedCardState = updateCardState(prevState.walletCardState)
prevState.copy(walletCardState = updatedCardState)
}
else -> {
prevState
}
}
}
override fun transform(walletUM: WalletUM): WalletUM {
return walletUM
}
private fun updateCardState(cardState: WalletCardState): WalletCardState {
val additionalInfo = WalletAdditionalInfo(
hideable = false,
content = resourceReference(
id = R.string.initial_wallet_sync_restore_progress,
formatArgs = wrappedList(progressPercent),
),
shouldShowProgress = true,
)
return when (cardState) {
is WalletCardState.Loading -> {
cardState.copy(additionalInfo = additionalInfo)
}
is WalletCardState.Content -> {
cardState.copy(additionalInfo = additionalInfo)
}
else -> cardState
}
}
}

View file

@ -12,6 +12,7 @@ import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.*
@ -164,6 +165,7 @@ private fun CardContainer(state: WalletCardState, isBalanceHidden: Boolean, item
}
AdditionalInfo(
text = additionalText,
showProgress = state.additionalInfo?.shouldShowProgress == true,
modifier = Modifier.conditional(
state.imageResId == null,
) { fillMaxWidth() },
@ -298,7 +300,7 @@ private fun Modifier.nonContentBalanceSize(dimens: TangemDimens): Modifier {
}
@Composable
private fun AdditionalInfo(text: TextReference?, modifier: Modifier = Modifier) {
private fun AdditionalInfo(text: TextReference?, showProgress: Boolean, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = text,
label = "Update the additional text",
@ -309,7 +311,19 @@ private fun AdditionalInfo(text: TextReference?, modifier: Modifier = Modifier)
},
) { animatedText ->
if (animatedText != null) {
AdditionalInfoText(text = animatedText)
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
AdditionalInfoText(text = animatedText)
if (showProgress) {
CircularProgressIndicator(
modifier = Modifier
.size(TangemTheme.dimens.size16),
color = TangemTheme.colors.icon.accent,
strokeWidth = TangemTheme.dimens.size2,
)
}
}
} else {
RectangleShimmer(modifier = Modifier.nonContentAdditionalInfoSize(dimens = TangemTheme.dimens))
}