Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-30 13:55:43 +04:00
parent e5a5d5ef97
commit 2e59b7e116
28 changed files with 802 additions and 38 deletions

View file

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

View file

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

View file

@ -72,6 +72,19 @@ interface TokenDetailsClickIntents {
fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig)
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
fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency)
fun onOpenUrlClick(url: String)
@ -79,6 +92,4 @@ interface TokenDetailsClickIntents {
fun onConfirmDisposeExpressStatus()
fun onDisposeExpressStatus()
fun onYieldInfoClick()
}

View file

@ -155,13 +155,17 @@ internal class TokenDetailsModel @Inject constructor(
private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val yieldSupplyGetRewardsBalanceUseCase: YieldSupplyGetRewardsBalanceUseCase,
) : Model(), TokenDetailsClickIntents, YieldSupplyDepositedWarningComponent.ModelCallback {
private val signCloreMessageUseCase: SignCloreMessageUseCase,
) : Model(),
TokenDetailsClickIntents,
YieldSupplyDepositedWarningComponent.ModelCallback {
private val params = paramsContainer.require<TokenDetailsComponent.Params>()
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()
@ -193,6 +197,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,
@ -215,9 +240,6 @@ internal class TokenDetailsModel @Inject constructor(
TokenDetailsCurrencyStatusAnalyticsSender(analyticsEventsHandler)
}
private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency))
val uiState: StateFlow<TokenDetailsState> = internalUiState
init {
updateTopBarMenu()
initButtons()
@ -666,9 +688,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))
}
}
}
@ -785,7 +806,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(
@ -1265,11 +1287,26 @@ internal class TokenDetailsModel @Inject constructor(
}
private fun handleNavigationParam() {
if (params.navigationAction is NavigationAction.Staking) {
openStaking()
when (params.navigationAction) {
is NavigationAction.Staking -> openStaking()
is NavigationAction.CloreMigration -> onCloreMigrationClick()
is NavigationAction.YieldSupply,
null,
-> Unit
}
}
// region Clore migration
// TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY])
override fun onCloreMigrationClick() = cloreMigrationModel.onCloreMigrationClick()
override fun onCloreSignMessage(message: String) = cloreMigrationModel.onCloreSignMessage(message)
override fun onOpenCloreClaimPortal() = cloreMigrationModel.onOpenCloreClaimPortal()
// endregion Clore migration
private companion object {
const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L
}

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import arrow.core.Either
import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
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
@ -378,4 +379,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 = clickIntents::onDismissBottomSheet,
content = CloreMigrationBottomSheetConfig(
message = message,
signature = signature,
isSigningInProgress = isSigningInProgress,
onMessageChange = onMessageChange,
onSignClick = onSignClick,
onCopyClick = onCopyClick,
onOpenPortalClick = onOpenPortalClick,
),
),
)
}
fun getStateWithUpdatedCloreMigrationSignature(signature: String): TokenDetailsState {
val state = currentStateProvider()
val bottomSheetConfig = state.bottomSheetConfig ?: return state
val content = bottomSheetConfig.content as? CloreMigrationBottomSheetConfig ?: return state
return state.copy(
bottomSheetConfig = bottomSheetConfig.copy(
content = content.copy(
signature = signature,
isSigningInProgress = false,
),
),
)
}
fun getStateWithCloreMigrationSigning(): TokenDetailsState {
val state = currentStateProvider()
val bottomSheetConfig = state.bottomSheetConfig ?: return state
val content = bottomSheetConfig.content as? CloreMigrationBottomSheetConfig ?: return state
return state.copy(
bottomSheetConfig = bottomSheetConfig.copy(
content = content.copy(isSigningInProgress = true),
),
)
}
// endregion Clore migration
}

View file

@ -35,6 +35,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
@ -45,7 +47,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
// TODO: Split to blocks [REDACTED_JIRA]
@Suppress("LongMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
internal fun TokenDetailsScreen(
state: TokenDetailsState,
@ -164,17 +166,25 @@ internal fun TokenDetailsScreen(
TokenDetailsDialogs(state = state)
state.bottomSheetConfig?.let { config ->
when (config.content) {
is TokenReceiveBottomSheetConfig -> {
TokenReceiveBottomSheet(config = config)
}
is ChooseAddressBottomSheetConfig -> {
ChooseAddressBottomSheet(config = config)
}
is ExpressStatusBottomSheetConfig -> {
ExpressStatusBottomSheet(config = config)
}
TokenDetailsBottomSheets(state = state)
}
}
@Composable
private fun TokenDetailsBottomSheets(state: TokenDetailsState) {
state.bottomSheetConfig?.let { config ->
when (config.content) {
is TokenReceiveBottomSheetConfig -> {
TokenReceiveBottomSheet(config = config)
}
is ChooseAddressBottomSheetConfig -> {
ChooseAddressBottomSheet(config = config)
}
is ExpressStatusBottomSheetConfig -> {
ExpressStatusBottomSheet(config = config)
}
is CloreMigrationBottomSheetConfig -> {
CloreMigrationBottomSheet(config = config)
}
}
}

View file

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

View file

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

View file

@ -202,6 +202,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
navigationAction = navigationAction,
apy = apy,
)
is NavigationAction.CloreMigration -> Unit
}
}
@ -298,6 +299,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
},
)
}
is NavigationAction.CloreMigration -> return
}
analyticsEventHandler.send(event)
}

View file

@ -18,6 +18,7 @@ import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.requireColdWallet
@ -35,6 +36,7 @@ import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.models.UnlockWalletsError
import com.tangem.domain.wallets.usecase.*
@ -65,6 +67,8 @@ internal interface WalletWarningsClickIntents {
fun onAddBackupCardClick()
fun onCloreMigrationClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onCloseAlreadySignedHashesWarningClick()
fun onGenerateMissedAddressesClick(missedAddressCurrencies: List<CryptoCurrency>)
@ -146,6 +150,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 {

View file

@ -76,6 +76,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.UsedOutdatedData,
is WalletNotification.UnlockVisaAccess,
is WalletNotification.Warning.YeildSupplyApprove, // TODO apply correct event
is WalletNotification.CloreMigration,
-> null
is WalletNotification.FinishWalletActivation -> {
val activationState = if (warning.isBackupExists) {

View file

@ -33,6 +33,7 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.extensions.addIf
import com.tangem.utils.extensions.isPositive
import com.tangem.utils.extensions.orZero
@ -317,6 +318,29 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
element = WalletNotification.Warning.SomeNetworksUnreachable,
condition = flattenCurrencies.hasUnreachableNetworks(),
)
addCloreMigrationNotification(flattenCurrencies, clickIntents)
}
private fun MutableList<WalletNotification>.addCloreMigrationNotification(
flattenCurrencies: Lce<TokenListError, List<CryptoCurrencyStatus>>,
clickIntents: WalletClickIntents,
) {
val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return
add(
WalletNotification.CloreMigration(
onStartMigrationClick = { clickIntents.onCloreMigrationClick(cloreCurrency) },
),
)
}
private fun Lce<TokenListError, List<CryptoCurrencyStatus>>.findCloreCurrency(): CryptoCurrencyStatus? {
val currencies = getOrNull(isPartialContentAccepted = true) ?: return null
return currencies.find { currencyStatus ->
BlockchainUtils.isClore(currencyStatus.currency.network.rawId)
}
}
private fun MutableList<WalletNotification>.addPushReminderNotification(

View file

@ -414,4 +414,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_sheet_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,
),
),
)
}