Updated on 2026-08-14
This commit is contained in:
parent
26201c5c0b
commit
0be1358760
39 changed files with 581 additions and 124 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
package com.tangem.tap.di.domain
|
package com.tangem.tap.di.domain
|
||||||
|
|
||||||
|
import com.tangem.domain.card.BackupValidator
|
||||||
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
|
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
|
||||||
|
import com.tangem.domain.card.IsWalletBackupProblematicUseCase
|
||||||
import com.tangem.domain.card.ResetCardUseCase
|
import com.tangem.domain.card.ResetCardUseCase
|
||||||
import com.tangem.domain.card.SetCardWasScannedUseCase
|
import com.tangem.domain.card.SetCardWasScannedUseCase
|
||||||
import com.tangem.domain.card.repository.CardRepository
|
import com.tangem.domain.card.repository.CardRepository
|
||||||
|
|
@ -32,6 +34,16 @@ internal object CardDomainModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig)
|
fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideBackupValidator(): BackupValidator = BackupValidator()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideIsWalletBackupProblematicUseCase(backupValidator: BackupValidator): IsWalletBackupProblematicUseCase {
|
||||||
|
return IsWalletBackupProblematicUseCase(backupValidator = backupValidator)
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideDerivePublicKeysUseCase(derivationsRepository: DerivationsRepository): DerivePublicKeysUseCase {
|
fun provideDerivePublicKeysUseCase(derivationsRepository: DerivationsRepository): DerivePublicKeysUseCase {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.di.domain
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||||
|
import com.tangem.domain.feedback.SendBackupProblemEmailUseCase
|
||||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
|
@ -36,4 +37,16 @@ internal object FeedbackDomainModule {
|
||||||
fun provideSaveBlockchainErrorUseCase(feedbackRepository: FeedbackRepository): SaveBlockchainErrorUseCase {
|
fun provideSaveBlockchainErrorUseCase(feedbackRepository: FeedbackRepository): SaveBlockchainErrorUseCase {
|
||||||
return SaveBlockchainErrorUseCase(feedbackRepository = feedbackRepository)
|
return SaveBlockchainErrorUseCase(feedbackRepository = feedbackRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideSendBackupProblemEmailUseCase(
|
||||||
|
getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
|
||||||
|
sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||||
|
): SendBackupProblemEmailUseCase {
|
||||||
|
return SendBackupProblemEmailUseCase(
|
||||||
|
getWalletMetaInfoUseCase = getWalletMetaInfoUseCase,
|
||||||
|
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +31,8 @@ dependencies {
|
||||||
implementation(projects.domain.onramp.models)
|
implementation(projects.domain.onramp.models)
|
||||||
implementation(projects.domain.offramp)
|
implementation(projects.domain.offramp)
|
||||||
implementation(projects.domain.demo)
|
implementation(projects.domain.demo)
|
||||||
|
implementation(projects.domain.card)
|
||||||
|
implementation(projects.domain.feedback)
|
||||||
|
|
||||||
implementation(deps.lifecycle.compose)
|
implementation(deps.lifecycle.compose)
|
||||||
implementation(deps.compose.foundation)
|
implementation(deps.compose.foundation)
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,12 @@ import com.tangem.core.navigation.url.UrlOpener
|
||||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.message.DialogMessage
|
import com.tangem.core.ui.message.DialogMessage
|
||||||
|
import com.tangem.core.ui.message.dialog.Dialogs
|
||||||
import com.tangem.core.ui.message.SnackbarMessage
|
import com.tangem.core.ui.message.SnackbarMessage
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
|
import com.tangem.domain.card.IsWalletBackupProblematicUseCase
|
||||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||||
|
import com.tangem.domain.feedback.SendBackupProblemEmailUseCase
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.offramp.GetOfframpUrlUseCase
|
import com.tangem.domain.offramp.GetOfframpUrlUseCase
|
||||||
import com.tangem.domain.onramp.model.OnrampSource
|
import com.tangem.domain.onramp.model.OnrampSource
|
||||||
|
|
@ -23,9 +26,9 @@ import com.tangem.utils.Provider
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
class TokenActionsHandler @AssistedInject constructor(
|
class TokenActionsHandler @AssistedInject constructor(
|
||||||
|
|
@ -39,6 +42,8 @@ class TokenActionsHandler @AssistedInject constructor(
|
||||||
@Assisted private val onHandleQuickAction: (action: HandledQuickAction, shouldDismiss: Boolean) -> Unit,
|
@Assisted private val onHandleQuickAction: (action: HandledQuickAction, shouldDismiss: Boolean) -> Unit,
|
||||||
@Assisted private val coroutineScope: CoroutineScope,
|
@Assisted private val coroutineScope: CoroutineScope,
|
||||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||||
|
private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase,
|
||||||
|
private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase,
|
||||||
private val messageSender: UiMessageSender,
|
private val messageSender: UiMessageSender,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
@ -47,6 +52,8 @@ class TokenActionsHandler @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: CryptoCurrencyData) {
|
fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: CryptoCurrencyData) {
|
||||||
|
if (isTopUpBlockedByBackupError(action, cryptoCurrencyData.userWallet)) return
|
||||||
|
|
||||||
onHandleQuickAction(
|
onHandleQuickAction(
|
||||||
HandledQuickAction(
|
HandledQuickAction(
|
||||||
action = action,
|
action = action,
|
||||||
|
|
@ -80,6 +87,21 @@ class TokenActionsHandler @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isTopUpBlockedByBackupError(action: TokenActionsBSContentUM.Action, userWallet: UserWallet): Boolean {
|
||||||
|
val isBlockedAction = action == TokenActionsBSContentUM.Action.Buy ||
|
||||||
|
action == TokenActionsBSContentUM.Action.Receive ||
|
||||||
|
action == TokenActionsBSContentUM.Action.Exchange
|
||||||
|
if (!isBlockedAction) return false
|
||||||
|
if (!isWalletBackupProblematicUseCase(userWallet)) return false
|
||||||
|
|
||||||
|
messageSender.send(
|
||||||
|
Dialogs.backupErrorAddFundsDisabled(
|
||||||
|
onContactSupport = { coroutineScope.launch { sendBackupProblemEmailUseCase(userWallet.walletId) } },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleDemoMode(action: TokenActionsBSContentUM.Action, userWallet: UserWallet.Cold): Boolean {
|
private fun handleDemoMode(action: TokenActionsBSContentUM.Action, userWallet: UserWallet.Cold): Boolean {
|
||||||
val isDemoCard = isDemoCardUseCase.invoke(userWallet.cardId)
|
val isDemoCard = isDemoCardUseCase.invoke(userWallet.cardId)
|
||||||
val isNeededShowDemoWarning = isDemoCard && disabledActionsInDemoMode.contains(action)
|
val isNeededShowDemoWarning = isDemoCard && disabledActionsInDemoMode.contains(action)
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,15 @@ sealed class NotificationUM(val config: NotificationConfig) {
|
||||||
subtitle = resourceReference(R.string.send_notification_invalid_amount_text),
|
subtitle = resourceReference(R.string.send_notification_invalid_amount_text),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class DestinationBackupError(val onContactSupport: () -> Unit) : Error(
|
||||||
|
title = resourceReference(R.string.warning_backup_error_add_funds_title),
|
||||||
|
subtitle = resourceReference(R.string.warning_backup_error_add_funds_message),
|
||||||
|
buttonState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||||
|
text = resourceReference(R.string.common_contact_support),
|
||||||
|
onClick = onContactSupport,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
data class MinimumAmountError(val amount: String) : Error(
|
data class MinimumAmountError(val amount: String) : Error(
|
||||||
title = resourceReference(R.string.send_notification_invalid_amount_title),
|
title = resourceReference(R.string.send_notification_invalid_amount_title),
|
||||||
subtitle = resourceReference(
|
subtitle = resourceReference(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.content.res.Configuration
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import coil.compose.SubcomposeAsyncImage
|
import coil.compose.SubcomposeAsyncImage
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.LocalIndication
|
import androidx.compose.foundation.LocalIndication
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
|
@ -28,6 +29,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.components.*
|
import com.tangem.core.ui.components.*
|
||||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||||
|
|
@ -61,6 +63,7 @@ fun Notification(
|
||||||
titleColor: Color = TangemTheme.colors.text.primary1,
|
titleColor: Color = TangemTheme.colors.text.primary1,
|
||||||
subtitleColor: Color = TangemTheme.colors.text.tertiary,
|
subtitleColor: Color = TangemTheme.colors.text.tertiary,
|
||||||
containerColor: Color? = null,
|
containerColor: Color? = null,
|
||||||
|
borderColor: Color? = null,
|
||||||
iconTint: Color? = when (config.iconTint) {
|
iconTint: Color? = when (config.iconTint) {
|
||||||
NotificationConfig.IconTint.Unspecified -> null
|
NotificationConfig.IconTint.Unspecified -> null
|
||||||
NotificationConfig.IconTint.Accent -> TangemTheme.colors.icon.accent
|
NotificationConfig.IconTint.Accent -> TangemTheme.colors.icon.accent
|
||||||
|
|
@ -75,6 +78,7 @@ fun Notification(
|
||||||
onCloseClick = config.onCloseClick,
|
onCloseClick = config.onCloseClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
|
borderColor = borderColor,
|
||||||
isEnabled = isEnabled,
|
isEnabled = isEnabled,
|
||||||
) {
|
) {
|
||||||
MainContent(
|
MainContent(
|
||||||
|
|
@ -100,6 +104,7 @@ internal fun NotificationBaseContainer(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
isEnabled: Boolean = true,
|
isEnabled: Boolean = true,
|
||||||
containerColor: Color? = null,
|
containerColor: Color? = null,
|
||||||
|
borderColor: Color? = null,
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val tempContainerColor by rememberUpdatedState(
|
val tempContainerColor by rememberUpdatedState(
|
||||||
|
|
@ -118,6 +123,7 @@ internal fun NotificationBaseContainer(
|
||||||
enabled = onClick != null && isEnabled,
|
enabled = onClick != null && isEnabled,
|
||||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||||
color = containerColor ?: tempContainerColor,
|
color = containerColor ?: tempContainerColor,
|
||||||
|
border = borderColor?.let { BorderStroke(width = 1.dp, color = it) },
|
||||||
) {
|
) {
|
||||||
Box {
|
Box {
|
||||||
Column(
|
Column(
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,24 @@ object Dialogs {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog shown when adding funds (buy / receive / swap) is blocked because the wallet has a backup problem.
|
||||||
|
* Centralizes the wording and button behavior reused across wallet actions, token details, markets and swap.
|
||||||
|
*
|
||||||
|
* @param onContactSupport lambda invoked when the "Contact support" action is clicked
|
||||||
|
*/
|
||||||
|
fun backupErrorAddFundsDisabled(onContactSupport: () -> Unit): DialogMessage = DialogMessage(
|
||||||
|
title = resourceReference(R.string.warning_backup_error_add_funds_title),
|
||||||
|
message = resourceReference(R.string.warning_backup_error_add_funds_message),
|
||||||
|
firstActionBuilder = {
|
||||||
|
EventMessageAction(
|
||||||
|
title = resourceReference(R.string.common_contact_support),
|
||||||
|
onClick = onContactSupport,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
secondActionBuilder = { cancelAction() },
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Universal error dialog
|
* Universal error dialog
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ object TangemColorPalette {
|
||||||
// region Amaranth
|
// region Amaranth
|
||||||
val Amaranth = Color(0xFFFF3333)
|
val Amaranth = Color(0xFFFF3333)
|
||||||
val Amaranth_50 = Color(0x80FF3333)
|
val Amaranth_50 = Color(0x80FF3333)
|
||||||
|
val Amaranth_30 = Color(0x4DFF3333)
|
||||||
val Amaranth_20 = Color(0x33FF3333)
|
val Amaranth_20 = Color(0x33FF3333)
|
||||||
val Amaranth_10 = Color(0x1AFF3333)
|
val Amaranth_10 = Color(0x1AFF3333)
|
||||||
// endregion Amaranth
|
// endregion Amaranth
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ android {
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
api(projects.domain.account)
|
api(projects.domain.account)
|
||||||
|
api(projects.domain.card)
|
||||||
api(projects.domain.core)
|
api(projects.domain.core)
|
||||||
api(projects.domain.common)
|
api(projects.domain.common)
|
||||||
api(projects.domain.express)
|
api(projects.domain.express)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
|
||||||
import com.tangem.domain.account.status.utils.CryptoCurrencyMetadataCleaner
|
import com.tangem.domain.account.status.utils.CryptoCurrencyMetadataCleaner
|
||||||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||||
|
import com.tangem.domain.card.IsWalletBackupProblematicUseCase
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||||
import com.tangem.domain.express.ExpressServiceFetcher
|
import com.tangem.domain.express.ExpressServiceFetcher
|
||||||
|
|
@ -24,6 +25,7 @@ import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
|
||||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||||
|
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
|
@ -50,6 +52,20 @@ internal object AccountStatusUseCaseModule {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideGetBackupProblematicWalletForAddressUseCase(
|
||||||
|
getAccountCurrencyByAddressUseCase: GetAccountCurrencyByAddressUseCase,
|
||||||
|
getUserWalletUseCase: GetUserWalletUseCase,
|
||||||
|
isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase,
|
||||||
|
): GetBackupProblematicWalletForAddressUseCase {
|
||||||
|
return GetBackupProblematicWalletForAddressUseCase(
|
||||||
|
getAccountCurrencyByAddressUseCase = getAccountCurrencyByAddressUseCase,
|
||||||
|
getUserWalletUseCase = getUserWalletUseCase,
|
||||||
|
isWalletBackupProblematicUseCase = isWalletBackupProblematicUseCase,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideIsAccountsModeEnabledUseCase(
|
fun provideIsAccountsModeEnabledUseCase(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.tangem.domain.account.status.usecase
|
||||||
|
|
||||||
|
import com.tangem.domain.card.IsWalletBackupProblematicUseCase
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves [address] to one of the user's own wallets and returns its [UserWalletId] when that wallet
|
||||||
|
* has a backup error (e.g. a CardLinked card), or `null` otherwise.
|
||||||
|
*
|
||||||
|
* Shared by the send and swap flows to block topping up a wallet with a backup problem.
|
||||||
|
*/
|
||||||
|
class GetBackupProblematicWalletForAddressUseCase(
|
||||||
|
private val getAccountCurrencyByAddressUseCase: GetAccountCurrencyByAddressUseCase,
|
||||||
|
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||||
|
private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(address: String): UserWalletId? {
|
||||||
|
val walletId = getAccountCurrencyByAddressUseCase(address).getOrNull()?.account?.userWalletId ?: return null
|
||||||
|
val wallet = getUserWalletUseCase(walletId).getOrNull() ?: return null
|
||||||
|
return walletId.takeIf { isWalletBackupProblematicUseCase(wallet) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
package com.tangem.domain.account.status.usecase
|
||||||
|
|
||||||
|
import arrow.core.left
|
||||||
|
import arrow.core.none
|
||||||
|
import arrow.core.right
|
||||||
|
import arrow.core.some
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.domain.account.status.model.AccountCryptoCurrency
|
||||||
|
import com.tangem.domain.card.IsWalletBackupProblematicUseCase
|
||||||
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.wallets.models.GetUserWalletError
|
||||||
|
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||||
|
import io.mockk.clearMocks
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
internal class GetBackupProblematicWalletForAddressUseCaseTest {
|
||||||
|
|
||||||
|
private val getAccountCurrencyByAddressUseCase: GetAccountCurrencyByAddressUseCase = mockk()
|
||||||
|
private val getUserWalletUseCase: GetUserWalletUseCase = mockk()
|
||||||
|
private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase = mockk()
|
||||||
|
|
||||||
|
private val useCase = GetBackupProblematicWalletForAddressUseCase(
|
||||||
|
getAccountCurrencyByAddressUseCase = getAccountCurrencyByAddressUseCase,
|
||||||
|
getUserWalletUseCase = getUserWalletUseCase,
|
||||||
|
isWalletBackupProblematicUseCase = isWalletBackupProblematicUseCase,
|
||||||
|
)
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
|
clearMocks(getAccountCurrencyByAddressUseCase, getUserWalletUseCase, isWalletBackupProblematicUseCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `returns null when address is not resolved to an account`() = runTest {
|
||||||
|
coEvery { getAccountCurrencyByAddressUseCase(ADDRESS) } returns none()
|
||||||
|
|
||||||
|
assertThat(useCase(ADDRESS)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `returns null when destination wallet is not found`() = runTest {
|
||||||
|
coEvery { getAccountCurrencyByAddressUseCase(ADDRESS) } returns accountCurrency.some()
|
||||||
|
every { getUserWalletUseCase(walletId) } returns GetUserWalletError.UserWalletNotFound.left()
|
||||||
|
|
||||||
|
assertThat(useCase(ADDRESS)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `returns null when destination wallet is not problematic`() = runTest {
|
||||||
|
coEvery { getAccountCurrencyByAddressUseCase(ADDRESS) } returns accountCurrency.some()
|
||||||
|
every { getUserWalletUseCase(walletId) } returns userWallet.right()
|
||||||
|
every { isWalletBackupProblematicUseCase(userWallet) } returns false
|
||||||
|
|
||||||
|
assertThat(useCase(ADDRESS)).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `returns wallet id when destination wallet is problematic`() = runTest {
|
||||||
|
coEvery { getAccountCurrencyByAddressUseCase(ADDRESS) } returns accountCurrency.some()
|
||||||
|
every { getUserWalletUseCase(walletId) } returns userWallet.right()
|
||||||
|
every { isWalletBackupProblematicUseCase(userWallet) } returns true
|
||||||
|
|
||||||
|
assertThat(useCase(ADDRESS)).isEqualTo(walletId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val ADDRESS = "0x1234567890abcdef"
|
||||||
|
val walletId = UserWalletId("011")
|
||||||
|
val userWallet = mockk<UserWallet>()
|
||||||
|
val accountCurrency = mockk<AccountCryptoCurrency> {
|
||||||
|
every { account } returns mockk { every { userWalletId } returns walletId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import com.tangem.common.card.EllipticCurve
|
||||||
import com.tangem.domain.card.configs.CardConfig
|
import com.tangem.domain.card.configs.CardConfig
|
||||||
import com.tangem.domain.models.scan.CardDTO
|
import com.tangem.domain.models.scan.CardDTO
|
||||||
|
|
||||||
object BackupValidator {
|
class BackupValidator {
|
||||||
|
|
||||||
fun isValidFull(cardDTO: CardDTO): Boolean {
|
fun isValidFull(cardDTO: CardDTO): Boolean {
|
||||||
return validateBackupStatus(cardDTO) && validateCurves(cardDTO)
|
return validateBackupStatus(cardDTO) && validateCurves(cardDTO)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.tangem.domain.card
|
||||||
|
|
||||||
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth for "this wallet has a backup problem".
|
||||||
|
*
|
||||||
|
* A wallet is considered problematic when it is backed by a physical card whose backup was not
|
||||||
|
* completed correctly (e.g. a card stuck in the `CardLinked` state) or when a backup error has
|
||||||
|
* been persisted for it. Such wallets must show a prominent warning and have all top-up
|
||||||
|
* operations blocked until the user resolves the issue with support.
|
||||||
|
*
|
||||||
|
* Local detection only. Can later be backed by a backend flag without changing call sites.
|
||||||
|
*/
|
||||||
|
class IsWalletBackupProblematicUseCase(
|
||||||
|
private val backupValidator: BackupValidator,
|
||||||
|
) {
|
||||||
|
|
||||||
|
operator fun invoke(userWallet: UserWallet): Boolean {
|
||||||
|
if (userWallet !is UserWallet.Cold) return false
|
||||||
|
return userWallet.hasBackupError || !backupValidator.isValidBackupStatus(userWallet.scanResponse.card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
package com.tangem.domain.card
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
import io.mockk.clearMocks
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
internal class IsWalletBackupProblematicUseCaseTest {
|
||||||
|
|
||||||
|
private val backupValidator: BackupValidator = mockk()
|
||||||
|
|
||||||
|
private val useCase = IsWalletBackupProblematicUseCase(backupValidator = backupValidator)
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
clearMocks(backupValidator)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `returns false for hot wallet`() {
|
||||||
|
val wallet = mockk<UserWallet.Hot>()
|
||||||
|
|
||||||
|
assertThat(useCase(wallet)).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `returns true for cold wallet with persisted backup error`() {
|
||||||
|
val wallet = coldWallet(hasBackupError = true)
|
||||||
|
every { backupValidator.isValidBackupStatus(any()) } returns true
|
||||||
|
|
||||||
|
assertThat(useCase(wallet)).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `returns true for cold wallet with invalid backup status`() {
|
||||||
|
val wallet = coldWallet(hasBackupError = false)
|
||||||
|
every { backupValidator.isValidBackupStatus(any()) } returns false
|
||||||
|
|
||||||
|
assertThat(useCase(wallet)).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `returns false for cold wallet without backup error and valid backup status`() {
|
||||||
|
val wallet = coldWallet(hasBackupError = false)
|
||||||
|
every { backupValidator.isValidBackupStatus(any()) } returns true
|
||||||
|
|
||||||
|
assertThat(useCase(wallet)).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun coldWallet(hasBackupError: Boolean): UserWallet.Cold = mockk {
|
||||||
|
every { this@mockk.hasBackupError } returns hasBackupError
|
||||||
|
every { scanResponse } returns mockk {
|
||||||
|
every { card } returns mockk()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,4 +17,7 @@ dependencies {
|
||||||
implementation(projects.domain.wallets.models)
|
implementation(projects.domain.wallets.models)
|
||||||
implementation(projects.domain.visa.models)
|
implementation(projects.domain.visa.models)
|
||||||
implementation(projects.domain.feedback.models)
|
implementation(projects.domain.feedback.models)
|
||||||
|
|
||||||
|
/** Testing libraries */
|
||||||
|
testImplementation(projects.test.core)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.tangem.domain.feedback
|
||||||
|
|
||||||
|
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
||||||
|
class SendBackupProblemEmailUseCase(
|
||||||
|
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
|
||||||
|
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(userWalletId: UserWalletId) {
|
||||||
|
val metaInfo = getWalletMetaInfoUseCase(userWalletId).getOrNull() ?: return
|
||||||
|
sendFeedbackEmailUseCase(type = FeedbackEmailType.BackupProblem(walletMetaInfo = metaInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package com.tangem.domain.feedback
|
||||||
|
|
||||||
|
import arrow.core.left
|
||||||
|
import arrow.core.right
|
||||||
|
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||||
|
import com.tangem.domain.feedback.models.WalletMetaInfo
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import io.mockk.clearMocks
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
internal class SendBackupProblemEmailUseCaseTest {
|
||||||
|
|
||||||
|
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase = mockk()
|
||||||
|
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase = mockk(relaxed = true)
|
||||||
|
|
||||||
|
private val useCase = SendBackupProblemEmailUseCase(
|
||||||
|
getWalletMetaInfoUseCase = getWalletMetaInfoUseCase,
|
||||||
|
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
|
||||||
|
)
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
|
clearMocks(getWalletMetaInfoUseCase, sendFeedbackEmailUseCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `does not send email when wallet meta info is unavailable`() = runTest {
|
||||||
|
coEvery { getWalletMetaInfoUseCase(walletId) } returns Throwable().left()
|
||||||
|
|
||||||
|
useCase(walletId)
|
||||||
|
|
||||||
|
coVerify(exactly = 0) { sendFeedbackEmailUseCase(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `sends backup problem email when wallet meta info is available`() = runTest {
|
||||||
|
val metaInfo = mockk<WalletMetaInfo>()
|
||||||
|
coEvery { getWalletMetaInfoUseCase(walletId) } returns metaInfo.right()
|
||||||
|
|
||||||
|
useCase(walletId)
|
||||||
|
|
||||||
|
coVerify(exactly = 1) {
|
||||||
|
sendFeedbackEmailUseCase(FeedbackEmailType.BackupProblem(walletMetaInfo = metaInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val walletId = UserWalletId("011")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ sealed class AddressValidation {
|
||||||
sealed class Error : AddressValidation() {
|
sealed class Error : AddressValidation() {
|
||||||
data object AddressInWallet : Error()
|
data object AddressInWallet : Error()
|
||||||
data object InvalidAddress : Error()
|
data object InvalidAddress : Error()
|
||||||
|
data object RecipientWalletBackupError : Error()
|
||||||
data class DataError(val throwable: Throwable) : Error()
|
data class DataError(val throwable: Throwable) : Error()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.model
|
|
||||||
|
|
||||||
import com.tangem.common.card.EllipticCurve
|
|
||||||
import com.tangem.domain.card.configs.CardConfig
|
|
||||||
import com.tangem.domain.models.scan.CardDTO
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class BackupValidator @Inject constructor() {
|
|
||||||
|
|
||||||
fun isValidFull(cardDTO: CardDTO): Boolean {
|
|
||||||
return validateBackupStatus(cardDTO) && validateCurves(cardDTO)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isValidBackupStatus(cardDTO: CardDTO): Boolean {
|
|
||||||
return validateBackupStatus(cardDTO)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateCurves(cardDTO: CardDTO): Boolean {
|
|
||||||
val config = CardConfig.createConfig(cardDTO)
|
|
||||||
// / Since the curve `bls12381_G2_AUG` was added later into first generation of wallets,
|
|
||||||
// we cannot determine whether this curve is missing due to an error or because the user
|
|
||||||
// did not want to recreate the wallet.
|
|
||||||
val expectedCurves = config.mandatoryCurves
|
|
||||||
.filterNot { it == EllipticCurve.Bls12381G2Aug }
|
|
||||||
val curves = cardDTO.wallets.map { it.curve }
|
|
||||||
for (expectedCurve in expectedCurves) {
|
|
||||||
val cardCurvesCount = curves.count { it == expectedCurve }
|
|
||||||
|
|
||||||
// missing curve
|
|
||||||
if (cardCurvesCount == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// duplicated curve
|
|
||||||
if (cardCurvesCount > 1) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateBackupStatus(cardDTO: CardDTO): Boolean {
|
|
||||||
val backupStatus = cardDTO.backupStatus
|
|
||||||
backupStatus ?: return true // for card with null backup status, validation should always returns true
|
|
||||||
return backupStatus !is CardDTO.BackupStatus.CardLinked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -12,6 +12,7 @@ import com.tangem.core.decompose.di.ModelScoped
|
||||||
import com.tangem.core.decompose.model.Model
|
import com.tangem.core.decompose.model.Model
|
||||||
import com.tangem.core.decompose.model.ParamsContainer
|
import com.tangem.core.decompose.model.ParamsContainer
|
||||||
import com.tangem.core.decompose.ui.UiMessageSender
|
import com.tangem.core.decompose.ui.UiMessageSender
|
||||||
|
import com.tangem.domain.card.BackupValidator
|
||||||
import com.tangem.domain.card.repository.CardRepository
|
import com.tangem.domain.card.repository.CardRepository
|
||||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||||
|
|
@ -212,7 +213,6 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
||||||
backupService.proceedBackup(iconScanRes = iconScanRes) { result ->
|
backupService.proceedBackup(iconScanRes = iconScanRes) { result ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is CompletionResult.Success -> {
|
is CompletionResult.Success -> {
|
||||||
val backupValidator = BackupValidator()
|
|
||||||
if (backupValidator.isValidBackupStatus(CardDTO(result.data)).not()) {
|
if (backupValidator.isValidBackupStatus(CardDTO(result.data)).not()) {
|
||||||
hasWalletBackupError = true
|
hasWalletBackupError = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import com.tangem.common.core.TangemSdkError
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.decompose.model.ParamsContainer
|
import com.tangem.core.decompose.model.ParamsContainer
|
||||||
import com.tangem.core.decompose.ui.UiMessageSender
|
import com.tangem.core.decompose.ui.UiMessageSender
|
||||||
|
import com.tangem.domain.card.BackupValidator
|
||||||
import com.tangem.domain.card.repository.CardRepository
|
import com.tangem.domain.card.repository.CardRepository
|
||||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||||
|
|
@ -97,6 +98,7 @@ internal class MultiWalletFinalizeModelTest {
|
||||||
every { backupService.backupCardsBatchIds } returns listOf(NON_RING_BATCH_ID, NON_RING_BATCH_ID)
|
every { backupService.backupCardsBatchIds } returns listOf(NON_RING_BATCH_ID, NON_RING_BATCH_ID)
|
||||||
every { backupService.currentState } returns BackupService.State.FinalizingPrimaryCard
|
every { backupService.currentState } returns BackupService.State.FinalizingPrimaryCard
|
||||||
coEvery { onboardingRepository.saveUnfinishedFinalizeOnboarding(any()) } just Runs
|
coEvery { onboardingRepository.saveUnfinishedFinalizeOnboarding(any()) } just Runs
|
||||||
|
every { backupValidator.isValidBackupStatus(any()) } returns true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -427,9 +429,7 @@ internal class MultiWalletFinalizeModelTest {
|
||||||
startFromFinalize = OnboardingMultiWalletState.FinalizeStage.ScanBackupFirstCard,
|
startFromFinalize = OnboardingMultiWalletState.FinalizeStage.ScanBackupFirstCard,
|
||||||
)
|
)
|
||||||
every { backupService.currentState } returns BackupService.State.FinalizingBackupCard(index = 1)
|
every { backupService.currentState } returns BackupService.State.FinalizingBackupCard(index = 1)
|
||||||
|
every { backupValidator.isValidBackupStatus(any()) } returns true
|
||||||
mockkConstructor(BackupValidator::class)
|
|
||||||
every { anyConstructed<BackupValidator>().isValidBackupStatus(any()) } returns true
|
|
||||||
|
|
||||||
val card: Card = mockk(relaxed = true)
|
val card: Card = mockk(relaxed = true)
|
||||||
val callbackSlot = slot<(CompletionResult<Card>) -> Unit>()
|
val callbackSlot = slot<(CompletionResult<Card>) -> Unit>()
|
||||||
|
|
@ -453,8 +453,6 @@ internal class MultiWalletFinalizeModelTest {
|
||||||
Assertions.assertEquals(MultiWalletFinalizeUM.Step.BackupDevice2, state.step)
|
Assertions.assertEquals(MultiWalletFinalizeUM.Step.BackupDevice2, state.step)
|
||||||
Assertions.assertEquals("backup-2-cccc".lastMaskedExpected(), state.cardNumber)
|
Assertions.assertEquals("backup-2-cccc".lastMaskedExpected(), state.cardNumber)
|
||||||
Assertions.assertTrue(events.contains(MultiWalletFinalizeComponent.Event.TwoBackupCardsAdded))
|
Assertions.assertTrue(events.contains(MultiWalletFinalizeComponent.Event.TwoBackupCardsAdded))
|
||||||
|
|
||||||
unmockkConstructor(BackupValidator::class)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.lastMaskedExpected(): String {
|
private fun String.lastMaskedExpected(): String {
|
||||||
|
|
|
||||||
|
|
@ -25,4 +25,17 @@ internal class SendDestinationAlertFactory @Inject constructor(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showRecipientBackupErrorAlert(onContactSupport: () -> Unit) {
|
||||||
|
messageSender.send(
|
||||||
|
DialogMessage(
|
||||||
|
title = resourceReference(id = R.string.warning_backup_error_add_funds_title),
|
||||||
|
message = resourceReference(id = R.string.warning_backup_error_add_funds_message),
|
||||||
|
firstAction = EventMessageAction(
|
||||||
|
title = resourceReference(R.string.common_contact_support),
|
||||||
|
onClick = onContactSupport,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.send.subcomponents.destination.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import arrow.core.getOrElse
|
import arrow.core.getOrElse
|
||||||
|
import arrow.core.left
|
||||||
import com.tangem.common.routing.AppRoute
|
import com.tangem.common.routing.AppRoute
|
||||||
import com.tangem.common.ui.navigationButtons.NavigationButton
|
import com.tangem.common.ui.navigationButtons.NavigationButton
|
||||||
import com.tangem.common.ui.navigationButtons.NavigationUM
|
import com.tangem.common.ui.navigationButtons.NavigationUM
|
||||||
|
|
@ -12,12 +13,15 @@ import com.tangem.core.decompose.model.ParamsContainer
|
||||||
import com.tangem.core.decompose.navigation.Router
|
import com.tangem.core.decompose.navigation.Router
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
||||||
|
import com.tangem.domain.account.status.usecase.GetBackupProblematicWalletForAddressUseCase
|
||||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||||
|
import com.tangem.domain.feedback.SendBackupProblemEmailUseCase
|
||||||
import com.tangem.domain.models.account.AccountStatus
|
import com.tangem.domain.models.account.AccountStatus
|
||||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.network.CryptoCurrencyAddress
|
import com.tangem.domain.models.network.CryptoCurrencyAddress
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
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.models.wallet.isLocked
|
||||||
import com.tangem.domain.qrscanning.models.SourceType
|
import com.tangem.domain.qrscanning.models.SourceType
|
||||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||||
|
|
@ -48,6 +52,7 @@ import com.tangem.features.send.subcomponents.destination.model.transformers.Sen
|
||||||
import com.tangem.features.send.subcomponents.destination.model.transformers.SendDestinationValidationStartedTransformer
|
import com.tangem.features.send.subcomponents.destination.model.transformers.SendDestinationValidationStartedTransformer
|
||||||
import com.tangem.features.send.subcomponents.destination.ui.state.DestinationWalletUM
|
import com.tangem.features.send.subcomponents.destination.ui.state.DestinationWalletUM
|
||||||
import com.tangem.features.send.impl.R
|
import com.tangem.features.send.impl.R
|
||||||
|
import com.tangem.features.send.subcomponents.destination.SendDestinationAlertFactory
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import com.tangem.utils.coroutines.JobHolder
|
import com.tangem.utils.coroutines.JobHolder
|
||||||
import com.tangem.utils.coroutines.saveIn
|
import com.tangem.utils.coroutines.saveIn
|
||||||
|
|
@ -58,6 +63,7 @@ import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
|
|
@ -79,6 +85,9 @@ internal class SendDestinationModel @Inject constructor(
|
||||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
|
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
|
||||||
|
private val getBackupProblematicWalletForAddressUseCase: GetBackupProblematicWalletForAddressUseCase,
|
||||||
|
private val sendDestinationAlertFactory: SendDestinationAlertFactory,
|
||||||
|
private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase,
|
||||||
) : Model(), SendDestinationClickIntents {
|
) : Model(), SendDestinationClickIntents {
|
||||||
private val params: SendDestinationComponentParams = paramsContainer.require()
|
private val params: SendDestinationComponentParams = paramsContainer.require()
|
||||||
|
|
||||||
|
|
@ -95,6 +104,8 @@ internal class SendDestinationModel @Inject constructor(
|
||||||
|
|
||||||
private val validationJobHolder = JobHolder()
|
private val validationJobHolder = JobHolder()
|
||||||
|
|
||||||
|
private val backupProblematicWalletCache = AtomicReference<Pair<String, UserWalletId?>?>(null)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
configDestinationNavigation()
|
configDestinationNavigation()
|
||||||
subscribeOnQRScannerResult()
|
subscribeOnQRScannerResult()
|
||||||
|
|
@ -288,17 +299,41 @@ internal class SendDestinationModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun resolveBackupProblematicWallet(address: String): UserWalletId? {
|
||||||
|
backupProblematicWalletCache.get()?.let { if (it.first == address) return it.second }
|
||||||
|
|
||||||
|
return getBackupProblematicWalletForAddressUseCase(address)
|
||||||
|
.also { backupProblematicWalletCache.set(address to it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun contactBackupSupport(userWalletId: UserWalletId) {
|
||||||
|
modelScope.launch { sendBackupProblemEmailUseCase(userWalletId) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun validate(address: String, memo: String?, type: EnterAddressSource? = null) {
|
private fun validate(address: String, memo: String?, type: EnterAddressSource? = null) {
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
_uiState.update(SendDestinationValidationStartedTransformer)
|
_uiState.update(SendDestinationValidationStartedTransformer)
|
||||||
|
|
||||||
val addressValidationResult = validateWalletAddressUseCase(
|
var addressValidationResult = validateWalletAddressUseCase(
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
network = cryptoCurrency.network,
|
network = cryptoCurrency.network,
|
||||||
address = address,
|
address = address,
|
||||||
senderAddresses = senderAddresses.value,
|
senderAddresses = senderAddresses.value,
|
||||||
allowSelfSend = params.isAllowSelfSend,
|
allowSelfSend = params.isAllowSelfSend,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (addressValidationResult.isRight()) {
|
||||||
|
val problematicWalletId = resolveBackupProblematicWallet(address)
|
||||||
|
if (problematicWalletId != null) {
|
||||||
|
addressValidationResult = AddressValidation.Error.RecipientWalletBackupError.left()
|
||||||
|
if (type != null) {
|
||||||
|
sendDestinationAlertFactory.showRecipientBackupErrorAlert(
|
||||||
|
onContactSupport = { contactBackupSupport(problematicWalletId) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val memoValidationResult = validateWalletMemoUseCase(
|
val memoValidationResult = validateWalletMemoUseCase(
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
cryptoCurrency = cryptoCurrency,
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,7 @@ internal class SendDestinationValidationResultTransformer(
|
||||||
val isValidAddress = addressValidationResult.isRight()
|
val isValidAddress = addressValidationResult.isRight()
|
||||||
val isValidMemo = shouldDisableMemo || memoValidationResult.isRight()
|
val isValidMemo = shouldDisableMemo || memoValidationResult.isRight()
|
||||||
|
|
||||||
val addressErrorText = addressValidationResult.mapLeft { error ->
|
val addressErrorText = resolveAddressErrorText()
|
||||||
when (error) {
|
|
||||||
is AddressValidation.Error.DataError,
|
|
||||||
AddressValidation.Error.InvalidAddress,
|
|
||||||
-> R.string.send_recipient_address_error
|
|
||||||
AddressValidation.Error.AddressInWallet -> R.string.send_error_address_same_as_wallet
|
|
||||||
}
|
|
||||||
}.leftOrNull()
|
|
||||||
|
|
||||||
val blockchainAddress =
|
val blockchainAddress =
|
||||||
(addressValidationResult.getOrNull() as? AddressValidation.Success.ValidNamedAddress)?.blockchainAddress
|
(addressValidationResult.getOrNull() as? AddressValidation.Success.ValidNamedAddress)?.blockchainAddress
|
||||||
|
|
@ -61,6 +54,16 @@ internal class SendDestinationValidationResultTransformer(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolveAddressErrorText(): Int? = addressValidationResult.mapLeft { error ->
|
||||||
|
when (error) {
|
||||||
|
is AddressValidation.Error.DataError,
|
||||||
|
AddressValidation.Error.InvalidAddress,
|
||||||
|
-> R.string.send_recipient_address_error
|
||||||
|
AddressValidation.Error.AddressInWallet -> R.string.send_error_address_same_as_wallet
|
||||||
|
AddressValidation.Error.RecipientWalletBackupError -> R.string.warning_backup_error_add_funds_message
|
||||||
|
}
|
||||||
|
}.leftOrNull()
|
||||||
|
|
||||||
private fun buildMemoField(
|
private fun buildMemoField(
|
||||||
memoField: DestinationTextFieldUM.RecipientMemo?,
|
memoField: DestinationTextFieldUM.RecipientMemo?,
|
||||||
isValidMemo: Boolean,
|
isValidMemo: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ import com.tangem.core.decompose.model.Model
|
||||||
import com.tangem.core.decompose.model.ParamsContainer
|
import com.tangem.core.decompose.model.ParamsContainer
|
||||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||||
import com.tangem.core.ui.format.bigdecimal.format
|
import com.tangem.core.ui.format.bigdecimal.format
|
||||||
|
import com.tangem.domain.account.status.usecase.GetBackupProblematicWalletForAddressUseCase
|
||||||
import com.tangem.domain.express.models.ExpressError
|
import com.tangem.domain.express.models.ExpressError
|
||||||
|
import com.tangem.domain.feedback.SendBackupProblemEmailUseCase
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.transaction.usecase.IsMemoRequiredUseCase
|
import com.tangem.domain.transaction.usecase.IsMemoRequiredUseCase
|
||||||
import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger
|
import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger
|
||||||
import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact
|
import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact
|
||||||
|
|
@ -28,6 +31,7 @@ import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
|
|
@ -38,6 +42,8 @@ internal class SwapNotificationsModel @Inject constructor(
|
||||||
private val swapNotificationsUpdateTrigger: DefaultSwapNotificationsUpdateTrigger,
|
private val swapNotificationsUpdateTrigger: DefaultSwapNotificationsUpdateTrigger,
|
||||||
private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger,
|
private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger,
|
||||||
private val isMemoRequiredUseCase: IsMemoRequiredUseCase,
|
private val isMemoRequiredUseCase: IsMemoRequiredUseCase,
|
||||||
|
private val getBackupProblematicWalletForAddressUseCase: GetBackupProblematicWalletForAddressUseCase,
|
||||||
|
private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase,
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
paramsContainer: ParamsContainer,
|
paramsContainer: ParamsContainer,
|
||||||
) : Model() {
|
) : Model() {
|
||||||
|
|
@ -47,6 +53,8 @@ internal class SwapNotificationsModel @Inject constructor(
|
||||||
private var notificationData = params.swapNotificationData
|
private var notificationData = params.swapNotificationData
|
||||||
private var lastSentErrorKeys: Set<Pair<String, Map<String, String>>> = emptySet()
|
private var lastSentErrorKeys: Set<Pair<String, Map<String, String>>> = emptySet()
|
||||||
|
|
||||||
|
private val backupProblematicWalletCache = AtomicReference<Pair<String, UserWalletId?>?>(null)
|
||||||
|
|
||||||
val uiState: StateFlow<ImmutableList<NotificationUM>>
|
val uiState: StateFlow<ImmutableList<NotificationUM>>
|
||||||
field = MutableStateFlow<ImmutableList<NotificationUM>>(persistentListOf())
|
field = MutableStateFlow<ImmutableList<NotificationUM>>(persistentListOf())
|
||||||
|
|
||||||
|
|
@ -73,6 +81,7 @@ internal class SwapNotificationsModel @Inject constructor(
|
||||||
addInsufficientFundsNotification()
|
addInsufficientFundsNotification()
|
||||||
addExpressErrorNotification()
|
addExpressErrorNotification()
|
||||||
addDestinationTagRequiredNotification()
|
addDestinationTagRequiredNotification()
|
||||||
|
addDestinationBackupErrorNotification()
|
||||||
maybeAddPriceImpactNotification()
|
maybeAddPriceImpactNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,6 +143,25 @@ internal class SwapNotificationsModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun MutableList<NotificationUM>.addDestinationBackupErrorNotification() {
|
||||||
|
val destinationAddress = notificationData.destinationAddress
|
||||||
|
if (destinationAddress.isEmpty()) return
|
||||||
|
|
||||||
|
val problematicWalletId = resolveBackupProblematicWallet(destinationAddress) ?: return
|
||||||
|
add(
|
||||||
|
NotificationUM.Error.DestinationBackupError(
|
||||||
|
onContactSupport = { modelScope.launch { sendBackupProblemEmailUseCase(problematicWalletId) } },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun resolveBackupProblematicWallet(address: String): UserWalletId? {
|
||||||
|
backupProblematicWalletCache.get()?.let { if (it.first == address) return it.second }
|
||||||
|
|
||||||
|
return getBackupProblematicWalletForAddressUseCase(address)
|
||||||
|
.also { backupProblematicWalletCache.set(address to it) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun MutableList<NotificationUM>.addInsufficientFundsNotification() {
|
private fun MutableList<NotificationUM>.addInsufficientFundsNotification() {
|
||||||
val enteredFromAmount = notificationData.enteredFromAmount ?: return
|
val enteredFromAmount = notificationData.enteredFromAmount ?: return
|
||||||
val balance = notificationData.fromCryptoCurrencyStatus?.value?.amount ?: return
|
val balance = notificationData.fromCryptoCurrencyStatus?.value?.amount ?: return
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ dependencies {
|
||||||
implementation(projects.domain.express.models)
|
implementation(projects.domain.express.models)
|
||||||
implementation(projects.domain.account)
|
implementation(projects.domain.account)
|
||||||
implementation(projects.domain.account.status)
|
implementation(projects.domain.account.status)
|
||||||
|
implementation(projects.domain.card)
|
||||||
implementation(projects.domain.visa)
|
implementation(projects.domain.visa)
|
||||||
implementation(projects.domain.markets)
|
implementation(projects.domain.markets)
|
||||||
implementation(projects.domain.swap)
|
implementation(projects.domain.swap)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat
|
||||||
import com.tangem.core.ui.format.bigdecimal.format
|
import com.tangem.core.ui.format.bigdecimal.format
|
||||||
import com.tangem.core.ui.message.DialogMessage
|
import com.tangem.core.ui.message.DialogMessage
|
||||||
import com.tangem.core.ui.message.EventMessageAction
|
import com.tangem.core.ui.message.EventMessageAction
|
||||||
|
import com.tangem.core.ui.message.dialog.Dialogs
|
||||||
import com.tangem.core.ui.utils.parseBigDecimal
|
import com.tangem.core.ui.utils.parseBigDecimal
|
||||||
import com.tangem.core.ui.utils.parseBigDecimalOrNull
|
import com.tangem.core.ui.utils.parseBigDecimalOrNull
|
||||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||||
|
|
@ -42,6 +43,7 @@ import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||||
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
||||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||||
|
import com.tangem.domain.card.IsWalletBackupProblematicUseCase
|
||||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||||
|
|
@ -49,6 +51,7 @@ import com.tangem.domain.express.models.ExpressOperationType
|
||||||
import com.tangem.domain.express.models.ProviderFilterType
|
import com.tangem.domain.express.models.ProviderFilterType
|
||||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||||
|
import com.tangem.domain.feedback.SendBackupProblemEmailUseCase
|
||||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||||
|
|
@ -149,6 +152,8 @@ internal class SwapModel @Inject constructor(
|
||||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||||
private val shouldShowStoriesUseCase: ShouldShowStoriesUseCase,
|
private val shouldShowStoriesUseCase: ShouldShowStoriesUseCase,
|
||||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||||
|
private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase,
|
||||||
|
private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase,
|
||||||
private val swapInteractor: SwapInteractor,
|
private val swapInteractor: SwapInteractor,
|
||||||
private val swapTransferInteractor: SwapTransferInteractor,
|
private val swapTransferInteractor: SwapTransferInteractor,
|
||||||
private val swapTransferStateBuilder: SwapTransferStateBuilder,
|
private val swapTransferStateBuilder: SwapTransferStateBuilder,
|
||||||
|
|
@ -472,6 +477,12 @@ internal class SwapModel @Inject constructor(
|
||||||
val selectedCurrencyStatus = result.currency
|
val selectedCurrencyStatus = result.currency
|
||||||
val selectedAccount = result.account.account
|
val selectedAccount = result.account.account
|
||||||
|
|
||||||
|
if (!isFromDirection && isWalletBackupProblematicUseCase(selectedUserWallet)) {
|
||||||
|
router.pop()
|
||||||
|
showBackupErrorAlert(selectedUserWallet.walletId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val (fromSwapCurrencyStatus, toSwapCurrencyStatus) = if (isFromDirection) {
|
val (fromSwapCurrencyStatus, toSwapCurrencyStatus) = if (isFromDirection) {
|
||||||
SwapCurrencyStatus(
|
SwapCurrencyStatus(
|
||||||
userWallet = selectedUserWallet,
|
userWallet = selectedUserWallet,
|
||||||
|
|
@ -1862,6 +1873,14 @@ internal class SwapModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showBackupErrorAlert(userWalletId: UserWalletId) {
|
||||||
|
messageSender.send(
|
||||||
|
Dialogs.backupErrorAddFundsDisabled(
|
||||||
|
onContactSupport = { modelScope.launch { sendBackupProblemEmailUseCase(userWalletId) } },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun showTransactionErrorAlert(
|
private fun showTransactionErrorAlert(
|
||||||
error: SwapTransactionState.Error,
|
error: SwapTransactionState.Error,
|
||||||
onSupportClick: (String) -> Unit = ::onFailedTxEmailClick,
|
onSupportClick: (String) -> Unit = ::onFailedTxEmailClick,
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@ import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSy
|
||||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||||
|
import com.tangem.domain.card.IsWalletBackupProblematicUseCase
|
||||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||||
|
import com.tangem.domain.feedback.SendBackupProblemEmailUseCase
|
||||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
|
|
@ -101,6 +103,8 @@ internal abstract class SwapModelTestBase {
|
||||||
protected val getSwapUiModeUseCase: GetSwapUiModeUseCase = mockk(relaxed = true)
|
protected val getSwapUiModeUseCase: GetSwapUiModeUseCase = mockk(relaxed = true)
|
||||||
protected val setSwapUiModeUseCase: SetSwapUiModeUseCase = mockk(relaxed = true)
|
protected val setSwapUiModeUseCase: SetSwapUiModeUseCase = mockk(relaxed = true)
|
||||||
protected val calculateAmountUseCase: CalculateAmountUseCase = mockk(relaxed = true)
|
protected val calculateAmountUseCase: CalculateAmountUseCase = mockk(relaxed = true)
|
||||||
|
protected val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase = mockk(relaxed = true)
|
||||||
|
protected val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase = mockk(relaxed = true)
|
||||||
|
|
||||||
private val chooseTokenBridgeFactory: ChooseTokenBridge.Factory = mockk(relaxed = true)
|
private val chooseTokenBridgeFactory: ChooseTokenBridge.Factory = mockk(relaxed = true)
|
||||||
private val getUserCountryUseCase: GetUserCountryUseCase = mockk(relaxed = true)
|
private val getUserCountryUseCase: GetUserCountryUseCase = mockk(relaxed = true)
|
||||||
|
|
@ -171,6 +175,8 @@ internal abstract class SwapModelTestBase {
|
||||||
getSwapUiModeUseCase = getSwapUiModeUseCase,
|
getSwapUiModeUseCase = getSwapUiModeUseCase,
|
||||||
setSwapUiModeUseCase = setSwapUiModeUseCase,
|
setSwapUiModeUseCase = setSwapUiModeUseCase,
|
||||||
calculateAmountUseCase = calculateAmountUseCase,
|
calculateAmountUseCase = calculateAmountUseCase,
|
||||||
|
isWalletBackupProblematicUseCase = isWalletBackupProblematicUseCase,
|
||||||
|
sendBackupProblemEmailUseCase = sendBackupProblemEmailUseCase,
|
||||||
)
|
)
|
||||||
|
|
||||||
// region builders
|
// region builders
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ dependencies {
|
||||||
implementation(projects.domain.demo)
|
implementation(projects.domain.demo)
|
||||||
implementation(projects.domain.dynamicAddresses)
|
implementation(projects.domain.dynamicAddresses)
|
||||||
implementation(projects.domain.dynamicAddresses.models)
|
implementation(projects.domain.dynamicAddresses.models)
|
||||||
|
implementation(projects.domain.feedback)
|
||||||
implementation(projects.domain.markets.models)
|
implementation(projects.domain.markets.models)
|
||||||
implementation(projects.domain.models)
|
implementation(projects.domain.models)
|
||||||
implementation(projects.domain.notifications.models)
|
implementation(projects.domain.notifications.models)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.extensions.wrappedList
|
import com.tangem.core.ui.extensions.wrappedList
|
||||||
import com.tangem.core.ui.message.DialogMessage
|
import com.tangem.core.ui.message.DialogMessage
|
||||||
import com.tangem.core.ui.message.EventMessageAction
|
import com.tangem.core.ui.message.EventMessageAction
|
||||||
|
import com.tangem.core.ui.message.dialog.Dialogs
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.features.tokendetails.impl.R
|
import com.tangem.features.tokendetails.impl.R
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -70,4 +71,8 @@ internal class TokenDetailsDialogFactory @Inject constructor(
|
||||||
fun showError(text: TextReference) {
|
fun showError(text: TextReference) {
|
||||||
uiMessageSender.send(DialogMessage(message = text))
|
uiMessageSender.send(DialogMessage(message = text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showBackupError(onContactSupport: () -> Unit) {
|
||||||
|
uiMessageSender.send(Dialogs.backupErrorAddFundsDisabled(onContactSupport = onContactSupport))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -53,6 +53,8 @@ import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
|
||||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||||
|
import com.tangem.domain.card.IsWalletBackupProblematicUseCase
|
||||||
|
import com.tangem.domain.feedback.SendBackupProblemEmailUseCase
|
||||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||||
import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains
|
import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains
|
||||||
import com.tangem.domain.models.StatusSource
|
import com.tangem.domain.models.StatusSource
|
||||||
|
|
@ -159,6 +161,8 @@ internal class TokenDetailsModel @Inject constructor(
|
||||||
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
|
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
|
||||||
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
|
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
|
||||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||||
|
private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase,
|
||||||
|
private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase,
|
||||||
private val associateAssetUseCase: AssociateAssetUseCase,
|
private val associateAssetUseCase: AssociateAssetUseCase,
|
||||||
private val retryIncompleteTransactionUseCase: RetryIncompleteTransactionUseCase,
|
private val retryIncompleteTransactionUseCase: RetryIncompleteTransactionUseCase,
|
||||||
private val openTrustlineUseCase: OpenTrustlineUseCase,
|
private val openTrustlineUseCase: OpenTrustlineUseCase,
|
||||||
|
|
@ -588,6 +592,7 @@ internal class TokenDetailsModel @Inject constructor(
|
||||||
if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) {
|
if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (isTopUpBlockedByBackupError()) return
|
||||||
|
|
||||||
val status = cryptoCurrencyStatus ?: return
|
val status = cryptoCurrencyStatus ?: return
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
|
|
@ -710,6 +715,7 @@ internal class TokenDetailsModel @Inject constructor(
|
||||||
if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) {
|
if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (isTopUpBlockedByBackupError()) return
|
||||||
|
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
if (needShowYieldSupplyWarning()) {
|
if (needShowYieldSupplyWarning()) {
|
||||||
|
|
@ -824,6 +830,7 @@ internal class TokenDetailsModel @Inject constructor(
|
||||||
if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) {
|
if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (isTopUpBlockedByBackupError()) return
|
||||||
|
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
if (checkYieldSupply && needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)) {
|
if (checkYieldSupply && needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus)) {
|
||||||
|
|
@ -1199,6 +1206,19 @@ internal class TokenDetailsModel @Inject constructor(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isTopUpBlockedByBackupError(): Boolean {
|
||||||
|
if (!isWalletBackupProblematicUseCase(userWallet)) return false
|
||||||
|
|
||||||
|
dialogFactory.showBackupError(onContactSupport = ::contactBackupSupport)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun contactBackupSupport() {
|
||||||
|
modelScope.launch {
|
||||||
|
sendBackupProblemEmailUseCase(userWallet.walletId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun openStaking() {
|
private fun openStaking() {
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
getStakingAvailabilityUseCase.invokeSync(userWalletId, cryptoCurrency)
|
getStakingAvailabilityUseCase.invokeSync(userWalletId, cryptoCurrency)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.tangem.common.ui.tokens.getUnavailabilityReasonText
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||||
import com.tangem.core.analytics.models.AnalyticsParam
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
|
import com.tangem.core.analytics.models.Basic.ButtonSupport
|
||||||
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
|
import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent
|
||||||
import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
|
import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent
|
||||||
import com.tangem.core.decompose.di.ModelScoped
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
|
|
@ -27,10 +28,12 @@ import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase
|
||||||
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
|
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
|
||||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||||
import com.tangem.domain.appcurrency.extenstions.unwrap
|
import com.tangem.domain.appcurrency.extenstions.unwrap
|
||||||
|
import com.tangem.domain.card.IsWalletBackupProblematicUseCase
|
||||||
import com.tangem.domain.core.lce.Lce
|
import com.tangem.domain.core.lce.Lce
|
||||||
import com.tangem.domain.core.utils.lceError
|
import com.tangem.domain.core.utils.lceError
|
||||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||||
import com.tangem.domain.exchange.RampStateManager
|
import com.tangem.domain.exchange.RampStateManager
|
||||||
|
import com.tangem.domain.feedback.SendBackupProblemEmailUseCase
|
||||||
import com.tangem.domain.markets.TokenMarketParams
|
import com.tangem.domain.markets.TokenMarketParams
|
||||||
import com.tangem.domain.models.TokenReceiveConfig
|
import com.tangem.domain.models.TokenReceiveConfig
|
||||||
import com.tangem.domain.models.account.AccountId
|
import com.tangem.domain.models.account.AccountId
|
||||||
|
|
@ -58,6 +61,7 @@ import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
|
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
|
||||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||||
|
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||||
import com.tangem.feature.wallet.impl.R
|
import com.tangem.feature.wallet.impl.R
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.unwrap
|
import com.tangem.feature.wallet.presentation.wallet.domain.unwrap
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||||
|
|
@ -144,6 +148,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
||||||
private val isCryptoCurrencyCouldHideUseCase: IsCryptoCurrencyCouldHideUseCase,
|
private val isCryptoCurrencyCouldHideUseCase: IsCryptoCurrencyCouldHideUseCase,
|
||||||
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||||
private val uiMessageSender: UiMessageSender,
|
private val uiMessageSender: UiMessageSender,
|
||||||
|
private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase,
|
||||||
|
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||||
|
private val sendBackupProblemEmailUseCase: SendBackupProblemEmailUseCase,
|
||||||
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
|
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
|
||||||
|
|
||||||
override fun onSendClick(
|
override fun onSendClick(
|
||||||
|
|
@ -196,6 +203,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
||||||
|
|
||||||
event?.let { analyticsEventHandler.send(it) }
|
event?.let { analyticsEventHandler.send(it) }
|
||||||
|
|
||||||
|
if (isTopUpBlockedByBackupError(accountId.userWalletId)) return
|
||||||
|
|
||||||
modelScope.launch(dispatchers.main) {
|
modelScope.launch(dispatchers.main) {
|
||||||
if (needShowYieldSupplyWarning(cryptoCurrencyStatus)) {
|
if (needShowYieldSupplyWarning(cryptoCurrencyStatus)) {
|
||||||
stateHolder.hideBottomSheet()
|
stateHolder.hideBottomSheet()
|
||||||
|
|
@ -339,6 +348,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (handleUnavailabilityReason(unavailabilityReason)) return
|
if (handleUnavailabilityReason(unavailabilityReason)) return
|
||||||
|
if (isTopUpBlockedByBackupError(accountId.userWalletId)) return
|
||||||
|
|
||||||
appRouter.push(
|
appRouter.push(
|
||||||
AppRoute.Onramp(
|
AppRoute.Onramp(
|
||||||
|
|
@ -363,6 +373,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (handleUnavailabilityReason(unavailabilityReason)) return
|
if (handleUnavailabilityReason(unavailabilityReason)) return
|
||||||
|
if (isTopUpBlockedByBackupError(accountId.userWalletId)) return
|
||||||
|
|
||||||
modelScope.launch(dispatchers.main) {
|
modelScope.launch(dispatchers.main) {
|
||||||
if (needShowYieldSupplyWarning(cryptoCurrencyStatus)) {
|
if (needShowYieldSupplyWarning(cryptoCurrencyStatus)) {
|
||||||
|
|
@ -451,6 +462,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMultiWalletSwapClick(userWalletId: UserWalletId) {
|
override fun onMultiWalletSwapClick(userWalletId: UserWalletId) {
|
||||||
|
if (isTopUpBlockedByBackupError(userWalletId)) return
|
||||||
if (!isMultiWalletTokensLoaded()) return
|
if (!isMultiWalletTokensLoaded()) return
|
||||||
|
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
|
|
@ -469,6 +481,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMultiWalletBuyClick(userWalletId: UserWalletId, screenType: String) {
|
override fun onMultiWalletBuyClick(userWalletId: UserWalletId, screenType: String) {
|
||||||
|
if (isTopUpBlockedByBackupError(userWalletId)) return
|
||||||
|
|
||||||
onMultiWalletActionClick(
|
onMultiWalletActionClick(
|
||||||
statusFlow = rampStateManager.getExpressInitializationStatus(userWalletId),
|
statusFlow = rampStateManager.getExpressInitializationStatus(userWalletId),
|
||||||
route = AppRoute.BuyCrypto(userWalletId = userWalletId),
|
route = AppRoute.BuyCrypto(userWalletId = userWalletId),
|
||||||
|
|
@ -565,6 +579,26 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isTopUpBlockedByBackupError(userWalletId: UserWalletId): Boolean {
|
||||||
|
val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return false
|
||||||
|
if (!isWalletBackupProblematicUseCase(userWallet)) return false
|
||||||
|
|
||||||
|
stateHolder.hideBottomSheet()
|
||||||
|
uiMessageSender.send(
|
||||||
|
WalletAlertUM.addFundsDisabledForBackupError(
|
||||||
|
onContactSupport = { contactBackupSupport(userWallet) },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun contactBackupSupport(userWallet: UserWallet) {
|
||||||
|
analyticsEventHandler.send(ButtonSupport(source = AnalyticsParam.ScreensSources.Main))
|
||||||
|
modelScope.launch {
|
||||||
|
sendBackupProblemEmailUseCase(userWallet.walletId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun isMultiWalletTokensLoaded(): Boolean {
|
private fun isMultiWalletTokensLoaded(): Boolean {
|
||||||
return if (stateHolder.value.isRedesignEnabled) {
|
return if (stateHolder.value.isRedesignEnabled) {
|
||||||
val selectedWalletUM = stateHolder.getSelectedWalletUM() as? WalletUM.Content ?: return false
|
val selectedWalletUM = stateHolder.getSelectedWalletUM() as? WalletUM.Content ?: return false
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
package com.tangem.feature.wallet.presentation.wallet.domain
|
|
||||||
|
|
||||||
import com.tangem.common.card.EllipticCurve
|
|
||||||
import com.tangem.domain.card.configs.CardConfig
|
|
||||||
import com.tangem.domain.models.scan.CardDTO
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class BackupValidator @Inject constructor() {
|
|
||||||
|
|
||||||
fun isValidFull(cardDTO: CardDTO): Boolean {
|
|
||||||
return validateBackupStatus(cardDTO) && validateCurves(cardDTO)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isValidBackupStatus(cardDTO: CardDTO): Boolean {
|
|
||||||
return validateBackupStatus(cardDTO)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateCurves(cardDTO: CardDTO): Boolean {
|
|
||||||
val config = CardConfig.createConfig(cardDTO)
|
|
||||||
// / Since the curve `bls12381_G2_AUG` was added later into first generation of wallets,
|
|
||||||
// we cannot determine whether this curve is missing due to an error or because the user
|
|
||||||
// did not want to recreate the wallet.
|
|
||||||
val expectedCurves = config.mandatoryCurves
|
|
||||||
.filterNot { it == EllipticCurve.Bls12381G2Aug }
|
|
||||||
val curves = cardDTO.wallets.map { it.curve }
|
|
||||||
for (expectedCurve in expectedCurves) {
|
|
||||||
val cardCurvesCount = curves.count { it == expectedCurve }
|
|
||||||
|
|
||||||
// missing curve
|
|
||||||
if (cardCurvesCount == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// duplicated curve
|
|
||||||
if (cardCurvesCount > 1) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateBackupStatus(cardDTO: CardDTO): Boolean {
|
|
||||||
val backupStatus = cardDTO.backupStatus
|
|
||||||
backupStatus ?: return true // for card with null backup status, validation should always returns true
|
|
||||||
return backupStatus !is CardDTO.BackupStatus.CardLinked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,6 +13,7 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
|
||||||
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
|
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
|
||||||
import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase
|
import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase
|
||||||
import com.tangem.domain.card.CardTypesResolver
|
import com.tangem.domain.card.CardTypesResolver
|
||||||
|
import com.tangem.domain.card.IsWalletBackupProblematicUseCase
|
||||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||||
import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase
|
import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase
|
||||||
|
|
@ -56,7 +57,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||||
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||||
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
||||||
private val backupValidator: BackupValidator,
|
private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase,
|
||||||
private val notificationsRepository: NotificationsRepository,
|
private val notificationsRepository: NotificationsRepository,
|
||||||
private val accountDependencies: AccountDependencies,
|
private val accountDependencies: AccountDependencies,
|
||||||
private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase,
|
private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase,
|
||||||
|
|
@ -118,6 +119,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||||
val isAddFundsBannerShown = isAddFundsBannerVisible(accountStatusList.totalFiatBalance)
|
val isAddFundsBannerShown = isAddFundsBannerVisible(accountStatusList.totalFiatBalance)
|
||||||
|
|
||||||
buildList {
|
buildList {
|
||||||
|
addBackupErrorNotification(userWallet, clickIntents)
|
||||||
|
|
||||||
addUsedOutdatedDataNotification(accountStatusList.totalFiatBalance)
|
addUsedOutdatedDataNotification(accountStatusList.totalFiatBalance)
|
||||||
|
|
||||||
addAddFundsBanner(
|
addAddFundsBanner(
|
||||||
|
|
@ -126,7 +129,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||||
clickIntents = clickIntents,
|
clickIntents = clickIntents,
|
||||||
)
|
)
|
||||||
|
|
||||||
addCriticalNotifications(userWallet, clickIntents)
|
addCriticalNotifications(userWallet)
|
||||||
|
|
||||||
addUpgradeHotWalletPromoNotification(
|
addUpgradeHotWalletPromoNotification(
|
||||||
userWallet = userWallet,
|
userWallet = userWallet,
|
||||||
|
|
@ -270,20 +273,22 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MutableList<WalletNotification>.addCriticalNotifications(
|
private fun MutableList<WalletNotification>.addBackupErrorNotification(
|
||||||
userWallet: UserWallet,
|
userWallet: UserWallet,
|
||||||
clickIntents: WalletClickIntents,
|
clickIntents: WalletClickIntents,
|
||||||
) {
|
) {
|
||||||
|
addIf(
|
||||||
|
element = WalletNotification.Critical.BackupError { clickIntents.onBackupErrorClick() },
|
||||||
|
condition = isWalletBackupProblematicUseCase(userWallet),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableList<WalletNotification>.addCriticalNotifications(userWallet: UserWallet) {
|
||||||
if (userWallet !is UserWallet.Cold) {
|
if (userWallet !is UserWallet.Cold) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||||
addIf(
|
|
||||||
element = WalletNotification.Critical.BackupError { clickIntents.onBackupErrorClick() },
|
|
||||||
condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError,
|
|
||||||
)
|
|
||||||
|
|
||||||
addIf(
|
addIf(
|
||||||
element = WalletNotification.Critical.DevCard,
|
element = WalletNotification.Critical.DevCard,
|
||||||
condition = !cardTypesResolver.isReleaseFirmwareType(),
|
condition = !cardTypesResolver.isReleaseFirmwareType(),
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
|
||||||
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
|
import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress
|
||||||
import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase
|
import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase
|
||||||
import com.tangem.domain.card.CardTypesResolver
|
import com.tangem.domain.card.CardTypesResolver
|
||||||
|
import com.tangem.domain.card.IsWalletBackupProblematicUseCase
|
||||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||||
import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase
|
import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase
|
||||||
|
|
@ -44,7 +45,7 @@ import javax.inject.Inject
|
||||||
internal class GetWalletNotificationsFactory @Inject constructor(
|
internal class GetWalletNotificationsFactory @Inject constructor(
|
||||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||||
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
||||||
private val backupValidator: BackupValidator,
|
private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase,
|
||||||
private val accountDependencies: AccountDependencies,
|
private val accountDependencies: AccountDependencies,
|
||||||
private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase,
|
private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase,
|
||||||
private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase,
|
private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase,
|
||||||
|
|
@ -167,7 +168,7 @@ internal class GetWalletNotificationsFactory @Inject constructor(
|
||||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||||
addIf(
|
addIf(
|
||||||
element = WalletNotificationUM.BackupError { clickIntents.onSupportClick() },
|
element = WalletNotificationUM.BackupError { clickIntents.onSupportClick() },
|
||||||
condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError,
|
condition = isWalletBackupProblematicUseCase(userWallet),
|
||||||
)
|
)
|
||||||
|
|
||||||
addIf(
|
addIf(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import com.tangem.core.ui.extensions.WrappedList
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.message.DialogMessage
|
import com.tangem.core.ui.message.DialogMessage
|
||||||
import com.tangem.core.ui.message.EventMessageAction
|
import com.tangem.core.ui.message.EventMessageAction
|
||||||
|
import com.tangem.core.ui.message.dialog.Dialogs
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.feature.wallet.impl.R
|
import com.tangem.feature.wallet.impl.R
|
||||||
|
|
||||||
|
|
@ -55,6 +56,10 @@ internal object WalletAlertUM {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addFundsDisabledForBackupError(onContactSupport: () -> Unit): DialogMessage {
|
||||||
|
return Dialogs.backupErrorAddFundsDisabled(onContactSupport = onContactSupport)
|
||||||
|
}
|
||||||
|
|
||||||
fun insufficientTokensCountForSwapping(): DialogMessage {
|
fun insufficientTokensCountForSwapping(): DialogMessage {
|
||||||
return DialogMessage(
|
return DialogMessage(
|
||||||
title = resourceReference(R.string.action_buttons_swap_no_tokens_added_alert_title),
|
title = resourceReference(R.string.action_buttons_swap_no_tokens_added_alert_title),
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,9 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BackupError(val onSupportClick: () -> Unit) : Critical(
|
data class BackupError(val onSupportClick: () -> Unit) : Critical(
|
||||||
title = resourceReference(R.string.warning_backup_errors_title),
|
title = resourceReference(R.string.warning_backup_error_attention_title),
|
||||||
subtitle = resourceReference(R.string.warning_backup_errors_message),
|
subtitle = resourceReference(R.string.warning_backup_error_attention_message),
|
||||||
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
buttonsState = ButtonsState.PrimaryButtonConfig(
|
||||||
text = resourceReference(id = R.string.common_contact_support),
|
text = resourceReference(id = R.string.common_contact_support),
|
||||||
onClick = onSupportClick,
|
onClick = onSupportClick,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import com.tangem.core.ui.extensions.annotatedReference
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||||
import com.tangem.core.ui.res.ForceDarkTheme
|
import com.tangem.core.ui.res.ForceDarkTheme
|
||||||
|
import com.tangem.core.ui.res.TangemColorPalette
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.test.WalletNotificationTestTags
|
import com.tangem.core.ui.test.WalletNotificationTestTags
|
||||||
import com.tangem.feature.wallet.impl.R
|
import com.tangem.feature.wallet.impl.R
|
||||||
|
|
@ -66,6 +67,16 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
|
||||||
subtitleColor = TangemTheme.colors.text.secondary,
|
subtitleColor = TangemTheme.colors.text.secondary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is WalletNotification.Critical.BackupError -> {
|
||||||
|
Notification(
|
||||||
|
config = item.config,
|
||||||
|
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||||
|
containerColor = TangemColorPalette.Amaranth_30,
|
||||||
|
borderColor = TangemColorPalette.Amaranth,
|
||||||
|
iconTint = TangemTheme.colors.icon.warning,
|
||||||
|
subtitleColor = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
}
|
||||||
is WalletNotification.CreateTangemPayAccount -> {
|
is WalletNotification.CreateTangemPayAccount -> {
|
||||||
CreatePaymentAccountNotification(
|
CreatePaymentAccountNotification(
|
||||||
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue