Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-19 17:09:41 +03:00
commit 5732d139f1
72 changed files with 946 additions and 620 deletions

View file

@ -277,8 +277,8 @@ dependencies {
debugImplementation(projects.features.kyc.impl)
internalImplementation(projects.features.kyc.impl)
mockedImplementation(projects.features.kyc.impl)
releaseImplementation(projects.features.kyc.mock)
externalImplementation(projects.features.kyc.mock)
releaseImplementation(projects.features.kyc.impl)
externalImplementation(projects.features.kyc.impl)
implementation(projects.features.welcome.api)
implementation(projects.features.welcome.impl)
implementation(projects.features.createWalletSelection.api)

@ -1 +1 @@
Subproject commit 76978242504ab90803b99e2b0575f7ebe8e69335
Subproject commit 5fc86d29bc2c0dc7c057ab0242b2cfa3e9f48daf

View file

@ -40,7 +40,6 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase
import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase
import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
@ -130,9 +129,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
lateinit var cardRepository: CardRepository
@Inject
lateinit var shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase
@Inject
lateinit var backupServiceHolder: BackupServiceHolder

View file

@ -1,15 +1,9 @@
package com.tangem.tap.di.domain
import com.tangem.domain.staking.*
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.repositories.StakeKitActionRepository
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakeKitRepository
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.staking.repositories.StakeKitTransactionHashRepository
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import com.tangem.domain.staking.repositories.*
import com.tangem.domain.staking.single.SingleStakingBalanceFetcher
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import dagger.Module
import dagger.Provides
@ -238,11 +232,7 @@ internal object StakingDomainModule {
@Provides
@Singleton
fun provideStakingApyFlowUseCase(
stakeKitRepository: StakeKitRepository,
p2pEthPoolRepository: P2PEthPoolRepository,
stakingFeatureToggles: StakingFeatureToggles,
): StakingApyFlowUseCase {
return StakingApyFlowUseCase(stakeKitRepository, p2pEthPoolRepository, stakingFeatureToggles)
fun provideStakingApyFlowUseCase(stakingRepository: StakingRepository): StakingAvailabilityListUseCase {
return StakingAvailabilityListUseCase(stakingRepository)
}
}

View file

@ -3,6 +3,9 @@ package com.tangem.tap.domain.sdk.impl
import android.content.res.Resources
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.Log
import com.tangem.Message
import com.tangem.TangemSdk
@ -24,6 +27,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.WithdrawalSignatureResult
import com.tangem.domain.visa.model.*
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
@ -516,23 +520,44 @@ internal class DefaultTangemSdkManager(
override suspend fun tangemPayProduceInitialCredentials(
cardId: String,
): CompletionResult<TangemPayInitialCredentials> {
): Either<Throwable, TangemPayInitialCredentials> {
return coroutineScope {
runTaskAsyncReturnOnMain(
val result = runTaskAsyncReturnOnMain(
runnable = tangemPayChallengeTaskFactory.create(coroutineScope = this),
cardId = cardId,
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
)
return@coroutineScope when (result) {
is CompletionResult.Failure<*> -> result.error.left()
is CompletionResult.Success<TangemPayInitialCredentials> -> result.data.right()
}
}
}
override suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult<String> {
override suspend fun getWithdrawalSignature(
cardId: String,
hash: String,
): Either<Throwable, WithdrawalSignatureResult> {
return coroutineScope {
runTaskAsyncReturnOnMain(
val result = runTaskAsyncReturnOnMain(
runnable = TangemPaySignWithdrawalHashTask(cardId = cardId, hash = hash.hexToBytes()),
cardId = cardId,
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
)
return@coroutineScope when (result) {
is CompletionResult.Failure<*> -> {
if (result.error is TangemSdkError.UserCancelled) {
WithdrawalSignatureResult.Cancelled.right()
} else {
result.error.left()
}
}
is CompletionResult.Success<String> -> {
WithdrawalSignatureResult.Success(result.data).right()
}
}
}
}
// endregion

View file

@ -3,6 +3,7 @@ package com.tangem.tap.domain.sdk.impl
import android.content.res.Resources
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import arrow.core.Either
import com.tangem.Message
import com.tangem.common.CompletionResult
import com.tangem.common.KeyPair
@ -18,7 +19,11 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.*
import com.tangem.domain.pay.WithdrawalSignatureResult
import com.tangem.domain.visa.model.TangemPayInitialCredentials
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter
import com.tangem.operations.wallet.CreateWalletResponse
@ -215,11 +220,14 @@ class MockTangemSdkManager(
override suspend fun tangemPayProduceInitialCredentials(
cardId: String,
): CompletionResult<TangemPayInitialCredentials> {
): Either<Throwable, TangemPayInitialCredentials> {
error("Not implemented")
}
override suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult<String> {
override suspend fun getWithdrawalSignature(
cardId: String,
hash: String,
): Either<Throwable, WithdrawalSignatureResult> {
error("Not implemented")
}

View file

@ -3,7 +3,6 @@ package com.tangem.tap.domain.tasks.visa
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
@ -42,12 +41,14 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor(
private suspend fun runSuspend(session: CardSession): CompletionResult<TangemPayInitialCredentials> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.curve }
?: return CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)
val address = when (val derivationResult = runDerivationTask(session, wallet)) {
is CompletionResult.Failure<*> -> return CompletionResult.Failure(derivationResult.error)
is CompletionResult.Success<ExtendedPublicKey> -> generateAddressFromExtendedKey(derivationResult.data)
is CompletionResult.Success<ExtendedPublicKey> -> VisaUtilities.generateAddressFromExtendedKey(
extendedPublicKey = derivationResult.data,
)
}
val userWalletId = UserWalletIdBuilder.walletPublicKey(wallet.publicKey)
@ -119,14 +120,6 @@ class TangemPayGenerateAddressAndSignChallengeTask @AssistedInject constructor(
return deferred.await()
}
private fun generateAddressFromExtendedKey(extendedPublicKey: ExtendedPublicKey): String {
val derivationData = VisaUtilities.visaBlockchain.makeAddressesFromExtendedPublicKey(
extendedPublicKey = extendedPublicKey,
cachedIndex = null,
)
return derivationData.address
}
@AssistedFactory
interface Factory {
fun create(coroutineScope: CoroutineScope): TangemPayGenerateAddressAndSignChallengeTask

View file

@ -1,15 +1,11 @@
package com.tangem.tap.domain.tasks.visa
import com.tangem.blockchain.common.UnmarshalHelper
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
@ -40,7 +36,7 @@ class TangemPaySignWithdrawalHashTask(
private fun proceedSign(card: Card, session: CardSession, callback: CompletionCallback<String>) {
val derivationPath = VisaUtilities.customDerivationPath
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run {
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.curve } ?: run {
callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError))
return
}
@ -71,7 +67,7 @@ class TangemPaySignWithdrawalHashTask(
private fun signData(
targetWalletPublicKey: ByteArray,
derivationPath: DerivationPath?,
extendedPublicKey: ExtendedPublicKey?,
extendedPublicKey: ExtendedPublicKey,
session: CardSession,
callback: CompletionCallback<String>,
) {
@ -84,12 +80,11 @@ class TangemPaySignWithdrawalHashTask(
signTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended(
val rsvSignature = VisaUtilities.unmarshallSignature(
signature = result.data.signature,
hash = hash,
publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey()
?: targetWalletPublicKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().lowercase()
extendedPublicKey = extendedPublicKey,
)
callback(CompletionResult.Success(rsvSignature))
}

View file

@ -1,28 +1,19 @@
package com.tangem.tap.domain.tasks.visa
import arrow.core.getOrElse
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
import com.tangem.blockchain.common.UnmarshalHelper
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.sign.SignHashCommand
@ -46,11 +37,7 @@ class VisaCustomerWalletApproveTask(
return
}
if (card.settings.isHDWalletAllowed) {
proceedApprove(card, session, callback)
} else {
proceedApproveWithLegacyCard(card, session, callback)
}
proceedApprove(card, session, callback)
}
private fun proceedApprove(
@ -60,7 +47,7 @@ class VisaCustomerWalletApproveTask(
) {
val derivationPath = VisaUtilities.customDerivationPath
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run {
val wallet = card.wallets.firstOrNull { it.curve == VisaUtilities.curve } ?: run {
callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError))
return
}
@ -114,43 +101,15 @@ class VisaCustomerWalletApproveTask(
)
}
private fun proceedApproveWithLegacyCard(
card: Card,
session: CardSession,
callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
) {
val publicKey = findKeyWithoutDerivation(
targetAddress = visaDataForApprove.targetAddress,
card = CardDTO(card),
).getOrElse { error ->
callback(CompletionResult.Failure(error.tangemError))
return
}
signApproveData(
targetWalletPublicKey = publicKey,
derivationPath = null,
extendedPublicKey = null,
session = session,
callback = callback,
)
}
// TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK
private fun hashPersonalMessage(message: ByteArray): ByteArray {
val prefix = "\u0019Ethereum Signed Message:\n${message.size}".toByteArray()
return (prefix + message).toKeccak()
}
private fun signApproveData(
targetWalletPublicKey: ByteArray,
derivationPath: DerivationPath?,
extendedPublicKey: ExtendedPublicKey?,
extendedPublicKey: ExtendedPublicKey,
session: CardSession,
callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
) {
val content = "Tangem Pay wants to sign in with your account. Nonce: ${visaDataForApprove.hashToSign}"
val hash = hashPersonalMessage(content.toByteArray(Charsets.UTF_8))
val content = VisaUtilities.signWithNonceMessage(visaDataForApprove.hashToSign)
val hash = VisaUtilities.hashPersonalMessage(content.toByteArray(Charsets.UTF_8))
val signTask = SignHashCommand(
hash = hash,
@ -161,36 +120,13 @@ class VisaCustomerWalletApproveTask(
signTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended(
val rsvSignature = VisaUtilities.unmarshallSignature(
signature = result.data.signature,
hash = hash,
publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey()
?: targetWalletPublicKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().lowercase()
scanCard(
session = session,
callback = callback,
signedData = visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress),
extendedPublicKey = extendedPublicKey,
)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
private fun scanCard(
signedData: VisaSignedDataByCustomerWallet,
session: CardSession,
callback: CompletionCallback<VisaSignedDataByCustomerWallet>,
) {
val scanTask = ScanTask()
scanTask.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(signedData))
visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))

View file

@ -0,0 +1,68 @@
package com.tangem.tap.features.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.components.DialogFullScreen
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.security.isSecurityExposed
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@Suppress("UnusedPrivateProperty")
class RootDetectedWarningComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
private val securityInfoProvider: DeviceSecurityInfoProvider,
private val settingsRepository: SettingsRepository,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
private val isShown = instanceKeeper.getOrCreateSimple { MutableStateFlow(false) }
suspend fun tryToShowWarningAndWaitContinuation() {
if (isShown.value) return
if (settingsRepository.isRootDetectedWarningShown().not() && securityInfoProvider.isSecurityExposed()) {
isShown.value = true
}
isShown.first { it == false } // Wait until the warning is dismissed
}
@Composable
override fun Content(modifier: Modifier) {
val isShownState by isShown.collectAsStateWithLifecycle()
if (isShownState) {
DialogFullScreen(onDismissRequest = {}) {
RootDetectedWarningContent(
modifier = modifier,
onContinueClick = remember(this) { ::onContinueClick },
)
}
}
}
private fun onContinueClick() {
componentScope.launch {
settingsRepository.setRootDetectedWarningShown(true)
isShown.value = false
}
}
@AssistedFactory
interface Factory : ComponentFactory<Unit, RootDetectedWarningComponent> {
override fun create(context: AppComponentContext, params: Unit): RootDetectedWarningComponent
}
}

View file

@ -0,0 +1,93 @@
package com.tangem.tap.features.root
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.icons.HighlightedIcon
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.wallet.R
@Composable
internal fun RootDetectedWarningContent(modifier: Modifier = Modifier, onContinueClick: () -> Unit = {}) {
Column(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary)
.statusBarsPadding()
.padding(horizontal = 16.dp),
) {
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center,
) {
InfoBlock(
modifier = Modifier.padding(top = 48.dp, bottom = 24.dp),
)
}
PrimaryButton(
modifier = Modifier
.navigationBarsPadding()
.padding(bottom = 16.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.common_understand_continue),
onClick = onContinueClick,
)
}
}
@Composable
private fun InfoBlock(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
HighlightedIcon(
icon = R.drawable.ic_alert_circle_24,
iconTint = TangemTheme.colors.icon.warning,
)
SpacerH(20.dp)
Text(
text = stringResourceSafe(R.string.root_detected_warning_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH(12.dp)
Text(
modifier = Modifier.padding(horizontal = 24.dp),
text = stringResourceSafe(R.string.root_detected_warning_description),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
}
@Preview
@Composable
private fun Preview() {
TangemThemePreview {
RootDetectedWarningContent()
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.tap.features.welcome.model
import com.tangem.common.core.TangemError
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -28,6 +28,7 @@ import javax.inject.Inject
internal class WelcomeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val appFinisher: AppFinisher,
private val analyticsEventsHandler: AnalyticsEventHandler,
paramsContainer: ParamsContainer,
) : Model(), StoreSubscriber<WelcomeState> {
@ -54,12 +55,12 @@ internal class WelcomeModel @Inject constructor(
}
private fun unlockWallets() {
Analytics.send(SignIn.ButtonBiometricSignIn())
analyticsEventsHandler.send(SignIn.ButtonBiometricSignIn())
store.dispatch(WelcomeAction.ProceedWithBiometrics)
}
private fun scanCard() {
Analytics.send(SignIn.ButtonCardSignIn())
analyticsEventsHandler.send(SignIn.ButtonCardSignIn())
store.dispatch(WelcomeAction.ProceedWithCard)
}

View file

@ -47,6 +47,7 @@ internal fun RootContent(
modifier: Modifier = Modifier,
wcContent: @Composable (modifier: Modifier) -> Unit,
hotAccessCodeContent: @Composable (modifier: Modifier) -> Unit,
rootDetectedWarningContent: @Composable (modifier: Modifier) -> Unit,
) {
val context = LocalContext.current
@ -82,6 +83,8 @@ internal fun RootContent(
hotAccessCodeContent(Modifier.fillMaxSize())
rootDetectedWarningContent(Modifier.fillMaxSize())
TangemSnackbarHost(
modifier = Modifier
.align(Alignment.BottomCenter)

View file

@ -38,6 +38,7 @@ import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.hot.TangemHotSDKProxy
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.root.RootDetectedWarningComponent
import com.tangem.tap.routing.RootContent
import com.tangem.tap.routing.component.RoutingComponent
import com.tangem.tap.routing.component.RoutingComponent.Child
@ -64,6 +65,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val tangemHotSDKProxy: TangemHotSDKProxy,
private val hotAccessCodeRequestComponentFactory: HotAccessCodeRequestComponent.Factory,
private val hotAccessCodeRequesterProxy: HotWalletPasswordRequesterProxy,
private val rootDetectedWarningComponentFactory: RootDetectedWarningComponent.Factory,
private val userWalletsListRepository: UserWalletsListRepository,
private val cardRepository: CardRepository,
private val onboardingRepository: OnboardingRepository,
@ -85,6 +87,11 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
.create(child("hotAccessCodeRequestComponent"), Unit)
}
private val rootDetectedWarningComponent: RootDetectedWarningComponent by lazy {
rootDetectedWarningComponentFactory
.create(child("rootDetectedWarningComponent"), Unit)
}
private val navigation = navigationProvider.getOrCreateTyped<AppRoute>()
private val stack: Value<ChildStack<AppRoute, Child>> = childStack(
@ -134,6 +141,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private fun initializeInitialNavigation() {
if (initialStack.isNullOrEmpty()) {
componentScope.launch {
rootDetectedWarningComponent.tryToShowWarningAndWaitContinuation()
val initialRoute = resolveInitialRoute()
router.replaceAll(initialRoute)
}
@ -177,6 +185,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
modifier = modifier,
wcContent = { wcRoutingComponent.Content(it) },
hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) },
rootDetectedWarningContent = { rootDetectedWarningComponent.Content(it) },
)
}

View file

@ -108,6 +108,13 @@ object NotificationsFactory {
}
}
// Must be called last shown only when no other notifications exist
fun MutableList<NotificationUM>.addHighFeeNotificationIfNoOther(shouldShowHighFeeNotification: Boolean) {
if (shouldShowHighFeeNotification && this.isEmpty()) {
add(NotificationUM.Info.YieldSupplyHighNetworkFee)
}
}
fun MutableList<NotificationUM>.addReserveAmountErrorNotification(
reserveAmount: BigDecimal?,
sendingAmount: BigDecimal,

View file

@ -1,6 +1,5 @@
package com.tangem.common.ui.tokens
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.ui.R
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
@ -22,8 +21,8 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.yieldSupplyKey
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.isStakingSupported
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingOption
import com.tangem.domain.staking.model.common.RewardInfo
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
@ -46,7 +45,7 @@ import java.math.BigDecimal
class TokenItemStateConverter(
private val appCurrency: AppCurrency,
private val yieldModuleApyMap: Map<String, BigDecimal> = emptyMap(),
private val stakingApyMap: Map<String, List<StakingTarget>> = emptyMap(),
private val stakingApyMap: Map<CryptoCurrency, StakingAvailability> = emptyMap(),
private val yieldSupplyPromoBannerKey: String? = null,
private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = {
CryptoCurrencyToIconStateConverter().convert(it)
@ -179,7 +178,7 @@ class TokenItemStateConverter(
private fun createTitleState(
currencyStatus: CryptoCurrencyStatus,
yieldModuleApyMap: Map<String, BigDecimal>,
stakingApyMap: Map<String, List<StakingTarget>>,
stakingApyMap: Map<CryptoCurrency, StakingAvailability>,
onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)?,
): TokenItemState.TitleState {
return when (val value = currencyStatus.value) {
@ -219,7 +218,7 @@ class TokenItemStateConverter(
private fun resolveEarnApy(
cryptoCurrencyStatus: CryptoCurrencyStatus,
yieldModuleApyMap: Map<String, BigDecimal>,
stakingApyMap: Map<String, List<StakingTarget>>,
stakingApyMap: Map<CryptoCurrency, StakingAvailability>,
): EarnApyInfo? {
val token = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
if (token != null && yieldModuleApyMap.isNotEmpty()) {
@ -274,35 +273,47 @@ class TokenItemStateConverter(
private fun findStakingRate(
currencyStatus: CryptoCurrencyStatus,
stakingApyMap: Map<String, List<StakingTarget>>,
stakingApyMap: Map<CryptoCurrency, StakingAvailability>,
): StakingLocalInfo {
val stakingKey = currencyStatus.currency.stakingKey()
val targets = stakingApyMap[stakingKey]
val stakingAvailability = stakingApyMap[currencyStatus.currency] as? StakingAvailability.Available
?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
val rewardInfo: RewardInfo? = if (stakeKitBalance != null) {
// StakeKit-specific: try to find rate from validator address
val targetsByAddress = targets.associateBy { it.address }
stakeKitBalance.balance.items
.mapNotNull { it.validatorAddress }
.firstNotNullOfOrNull { address -> targetsByAddress[address]?.rewardInfo }
?: targets
.filter { it.isPreferred }
.mapNotNull { it.rewardInfo }
val rateInfo = when (val stakingOptions = stakingAvailability.option) {
is StakingOption.P2PEthPool -> {
RewardInfo(
rate = stakingOptions.apy,
type = RewardType.APY,
)
}
is StakingOption.StakeKit -> if (stakeKitBalance != null) {
val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address }
stakeKitBalance.balance.items
.mapNotNull { it.validatorAddress }
.firstNotNullOfOrNull { address ->
validatorsByAddress[address]?.rewardInfo
} ?: stakingOptions.yield.validators
.filter { it.preferred }
.mapNotNull { validator ->
validator.rewardInfo
}
.maxByOrNull { it.rate }
} else {
targets
.mapNotNull { it.rewardInfo }
.maxByOrNull { it.rate }
} else {
stakingOptions.yield.validators
.filter { it.preferred }
.mapNotNull { validator ->
validator.rewardInfo
}
.maxByOrNull { it.rate }
}
}
return StakingLocalInfo(
rate = rewardInfo?.rate,
isActive = stakingBalance != null,
rewardType = rewardInfo?.type,
rate = rateInfo?.rate,
isActive = stakeKitBalance != null, // todo add p2p check
rewardType = rateInfo?.type,
)
}
@ -441,18 +452,6 @@ class TokenItemStateConverter(
}
fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = sources.total == StatusSource.CACHE
private fun CryptoCurrency.stakingKey(): String {
if (this is CryptoCurrency.Coin && !network.isStakingSupported) return ""
if (network.isStakingSupported && this !is CryptoCurrency.Coin) {
val isPolygonTokenOnEthereum = this is CryptoCurrency.Token &&
this.network.id.rawId.value == Blockchain.Ethereum.id &&
this.symbol == Blockchain.Polygon.currency
if (!isPolygonTokenOnEthereum) return ""
}
return "${id.rawCurrencyId}_$symbol"
}
}
private data class StakingLocalInfo(

View file

@ -6,9 +6,9 @@ import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class BalanceResponse(
@Json(name = "fiat") val fiat: FiatBalance,
@Json(name = "crypto") val crypto: CryptoBalance,
@Json(name = "available_for_withdrawal") val availableForWithdrawal: AvailableForWithdrawal,
@Json(name = "fiat") val fiat: FiatBalance?,
@Json(name = "crypto") val crypto: CryptoBalance?,
@Json(name = "available_for_withdrawal") val availableForWithdrawal: AvailableForWithdrawal?,
)
@JsonClass(generateAdapter = true)

View file

@ -77,6 +77,9 @@ enum class NetworkTypeDTO {
@Json(name = "canto")
CANTO,
@Json(name = "cardano")
CARDANO,
@Json(name = "chihuahua")
CHIHUAHUA,

View file

@ -27,6 +27,8 @@ object PreferencesKeys {
val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") }
val ROOT_DETECTED_WARNING_SHOWN_KEY by lazy { booleanPreferencesKey(name = "rootDetectedWarningShown") }
val SHOULD_SHOW_ASK_BIOMETRY_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") }
val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") }

View file

@ -33,6 +33,7 @@ object StakingNetworkTypeConverter : TwoWayConverter<NetworkTypeDTO, NetworkType
NetworkTypeDTO.BAND_PROTOCOL -> NetworkType.BAND_PROTOCOL
NetworkTypeDTO.BITSONG -> NetworkType.BITSONG
NetworkTypeDTO.CANTO -> NetworkType.CANTO
NetworkTypeDTO.CARDANO -> NetworkType.CARDANO
NetworkTypeDTO.CHIHUAHUA -> NetworkType.CHIHUAHUA
NetworkTypeDTO.COMDEX -> NetworkType.COMDEX
NetworkTypeDTO.COREUM -> NetworkType.COREUM
@ -106,6 +107,7 @@ object StakingNetworkTypeConverter : TwoWayConverter<NetworkTypeDTO, NetworkType
NetworkType.BAND_PROTOCOL -> NetworkTypeDTO.BAND_PROTOCOL
NetworkType.BITSONG -> NetworkTypeDTO.BITSONG
NetworkType.CANTO -> NetworkTypeDTO.CANTO
NetworkType.CARDANO -> NetworkTypeDTO.CARDANO
NetworkType.CHIHUAHUA -> NetworkTypeDTO.CHIHUAHUA
NetworkType.COMDEX -> NetworkTypeDTO.COMDEX
NetworkType.COREUM -> NetworkTypeDTO.COREUM

View file

@ -37,44 +37,46 @@ fun DialogFullScreen(
decorFitsSystemWindows = false,
),
content = {
val activityWindow = getActivityWindow()
val dialogWindow = getDialogWindow()
val parentView = LocalView.current.parent as View
SideEffect {
if (activityWindow != null && dialogWindow != null) {
val attributes = WindowManager.LayoutParams().apply {
copyFrom(activityWindow.attributes)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
} else {
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
}
type = dialogWindow.attributes.type
}
dialogWindow.attributes = attributes
parentView.layoutParams =
FrameLayout.LayoutParams(
activityWindow.decorView.width,
activityWindow.decorView.height,
)
}
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
val systemUiController = rememberSystemUiController(getActivityWindow())
val dialogSystemUiController = rememberSystemUiController(getDialogWindow())
ProvideSystemBarsIconsController {
val activityWindow = getActivityWindow()
val dialogWindow = getDialogWindow()
val parentView = LocalView.current.parent as View
SideEffect {
systemUiController.setSystemBarsColor(color = Color.Transparent)
dialogSystemUiController.setSystemBarsColor(color = Color.Transparent)
if (activityWindow != null && dialogWindow != null) {
val attributes = WindowManager.LayoutParams().apply {
copyFrom(activityWindow.attributes)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
} else {
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
}
type = dialogWindow.attributes.type
}
dialogWindow.attributes = attributes
parentView.layoutParams =
FrameLayout.LayoutParams(
activityWindow.decorView.width,
activityWindow.decorView.height,
)
}
}
}
SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not())
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
val systemUiController = rememberSystemUiController(getActivityWindow())
val dialogSystemUiController = rememberSystemUiController(getDialogWindow())
Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) {
content()
SideEffect {
systemUiController.setSystemBarsColor(color = Color.Transparent)
dialogSystemUiController.setSystemBarsColor(color = Color.Transparent)
}
}
SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not())
Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) {
content()
}
}
},
)

View file

@ -2,17 +2,13 @@ package com.tangem.core.ui.components.bottomsheets.message
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -25,6 +21,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.icons.HighlightedIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -144,20 +141,11 @@ private fun BottomSheetIcon(icon: MessageBottomSheetUMV2.Icon, modifier: Modifie
MessageBottomSheetUMV2.Icon.BackgroundType.Warning -> TangemTheme.colors.icon.warning
}
Box(
modifier = modifier
.size(TangemTheme.dimens.size56)
.clip(CircleShape)
.background(backgroundColor.copy(alpha = 0.1F)),
contentAlignment = Alignment.Center,
content = {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size32),
painter = painterResource(icon.res),
contentDescription = null,
tint = tint,
)
},
HighlightedIcon(
modifier = modifier,
icon = icon.res,
iconTint = tint,
backgroundColor = backgroundColor,
)
}

View file

@ -0,0 +1,39 @@
package com.tangem.core.ui.components.icons
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.res.TangemTheme
@Composable
fun HighlightedIcon(
@DrawableRes icon: Int,
iconTint: Color,
modifier: Modifier = Modifier,
backgroundColor: Color = iconTint,
) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size56)
.clip(CircleShape)
.background(backgroundColor.copy(alpha = 0.1F)),
contentAlignment = Alignment.Center,
content = {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size32),
painter = painterResource(icon),
contentDescription = null,
tint = iconTint,
)
},
)
}

View file

@ -68,6 +68,27 @@ fun CoroutineScope.launchOnCancellation(block: suspend () -> Unit) {
}
}
@Suppress("LongParameterList", "MagicNumber")
inline fun <T1, T2, T3, T4, T5, T6, R> combine6(
flow1: Flow<T1>,
flow2: Flow<T2>,
flow3: Flow<T3>,
flow4: Flow<T4>,
flow5: Flow<T5>,
flow6: Flow<T6>,
crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R,
): Flow<R> = combine(flow1, flow2, flow3, flow4, flow5, flow6) { arr ->
@Suppress("UNCHECKED_CAST")
transform(
arr[0] as T1,
arr[1] as T2,
arr[2] as T3,
arr[3] as T4,
arr[4] as T5,
arr[5] as T6,
)
}
@Suppress("LongParameterList", "MagicNumber")
inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine7(
flow1: Flow<T1>,

View file

@ -193,4 +193,15 @@ internal class DefaultSettingsRepository(
default = false,
)
}
override suspend fun isRootDetectedWarningShown(): Boolean {
return appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.ROOT_DETECTED_WARNING_SHOWN_KEY,
default = false,
)
}
override suspend fun setRootDetectedWarningShown(value: Boolean) {
appPreferencesStore.store(key = PreferencesKeys.ROOT_DETECTED_WARNING_SHOWN_KEY, value = value)
}
}

View file

@ -17,6 +17,7 @@ dependencies {
/** Project - Data */
implementation(projects.core.datasource)
implementation(projects.core.error)
implementation(projects.core.error.ext)
implementation(projects.core.security)
implementation(projects.data.common)
@ -60,6 +61,7 @@ dependencies {
/** Libs - Tangem */
implementation(tangemDeps.blockchain)
implementation(tangemDeps.card.core)
implementation(tangemDeps.hot.core)
implementation(projects.libs.tangemSdkApi)
/** DI */

View file

@ -1,42 +1,34 @@
package com.tangem.data.pay.datasource
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.WithdrawalSignatureResult
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.pay.model.WithdrawalSignatureResult
import com.tangem.domain.visa.model.TangemPayInitialCredentials
import com.tangem.sdk.api.TangemSdkManager
import javax.inject.Inject
internal class DefaultTangemPayAuthDataSource @Inject constructor(
private val tangemSdkManager: TangemSdkManager,
private val tangemPayHotSdkManager: TangemPayHotSdkManager,
) : TangemPayAuthDataSource {
override suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials> {
return when (val initialCredentials = tangemSdkManager.tangemPayProduceInitialCredentials(cardId = cardId)) {
is CompletionResult.Failure<*> -> initialCredentials.error.left()
is CompletionResult.Success<TangemPayInitialCredentials> -> initialCredentials.data.right()
override suspend fun produceInitialCredentials(
userWallet: UserWallet,
): Either<Throwable, TangemPayInitialCredentials> {
return when (userWallet) {
is UserWallet.Cold -> tangemSdkManager.tangemPayProduceInitialCredentials(cardId = userWallet.cardId)
is UserWallet.Hot -> tangemPayHotSdkManager.produceInitialCredentials(userWallet)
}
}
override suspend fun getWithdrawalSignature(
cardId: String,
userWallet: UserWallet,
hash: String,
): Either<Throwable, WithdrawalSignatureResult> {
return when (val signResult = tangemSdkManager.getWithdrawalSignature(cardId, hash)) {
is CompletionResult.Failure<*> -> {
if (signResult.error is TangemSdkError.UserCancelled) {
WithdrawalSignatureResult.Cancelled.right()
} else {
signResult.error.left()
}
}
is CompletionResult.Success<String> -> {
WithdrawalSignatureResult.Success(signResult.data).right()
}
return when (userWallet) {
is UserWallet.Cold -> tangemSdkManager.getWithdrawalSignature(cardId = userWallet.cardId, hash = hash)
is UserWallet.Hot -> tangemPayHotSdkManager.getWithdrawalSignature(hotWallet = userWallet, hash = hash)
}
}
}

View file

@ -0,0 +1,130 @@
package com.tangem.data.pay.datasource
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.Raise
import arrow.core.raise.either
import com.tangem.common.extensions.hexToBytes
import com.tangem.core.error.ext.tangemError
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.WithdrawalSignatureResult
import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.error.VisaCardScanError
import com.tangem.domain.visa.model.TangemPayInitialCredentials
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.DataToSign
import com.tangem.hot.sdk.model.DeriveWalletRequest
import com.tangem.hot.sdk.model.UnlockHotWallet
import javax.inject.Inject
internal class TangemPayHotSdkManager @Inject constructor(
private val hotWalletAccessor: HotWalletAccessor,
private val tangemHotSdk: TangemHotSdk,
private val tangemPayAuthRemoteDataSource: TangemPayRemoteDataSource,
) {
suspend fun produceInitialCredentials(hotWallet: UserWallet.Hot): Either<Throwable, TangemPayInitialCredentials> =
withUnlockedHotWallet(hotWallet) { unlockHotWallet ->
val extendedPublicKey = getExtendedPublicKey(unlockHotWallet = unlockHotWallet)
val address = VisaUtilities.generateAddressFromExtendedKey(extendedPublicKey)
val challenge = tangemPayAuthRemoteDataSource.getCustomerWalletAuthChallenge(
customerWalletAddress = address,
customerWalletId = hotWallet.walletId.stringValue,
).getOrElse { raise(it.tangemError) }
val content = VisaUtilities.signWithNonceMessage(challenge.challenge)
val hash = VisaUtilities.hashPersonalMessage(content.toByteArray(Charsets.UTF_8))
val signature = getSignature(
unlockHotWallet = unlockHotWallet,
hash = hash,
extendedPublicKey = extendedPublicKey,
)
val authTokens = tangemPayAuthRemoteDataSource.getTokenWithCustomerWallet(
sessionId = challenge.session.sessionId,
signature = signature,
nonce = challenge.challenge,
).getOrElse { raise(VisaActivationError.FailedRemoteState.tangemError) }
TangemPayInitialCredentials(
customerWalletAddress = address,
authTokens = authTokens,
)
}
suspend fun getWithdrawalSignature(
hotWallet: UserWallet.Hot,
hash: String,
): Either<Throwable, WithdrawalSignatureResult> = withUnlockedHotWallet(hotWallet) { unlockHotWallet ->
val signature = getSignature(
unlockHotWallet = unlockHotWallet,
hash = hash.hexToBytes(),
extendedPublicKey = getExtendedPublicKey(unlockHotWallet = unlockHotWallet),
)
WithdrawalSignatureResult.Success(signature)
}
private suspend fun Raise<Throwable>.getExtendedPublicKey(unlockHotWallet: UnlockHotWallet): ExtendedPublicKey {
val publicKeyResponse = tangemHotSdk.derivePublicKey(
unlockHotWallet = unlockHotWallet,
request = DeriveWalletRequest(
requests = listOf(
DeriveWalletRequest.Request(
curve = VisaUtilities.curve,
paths = listOf(VisaUtilities.customDerivationPath),
),
),
),
)
return publicKeyResponse.responses
.firstOrNull { it.curve == VisaUtilities.curve }
?.publicKeys[VisaUtilities.customDerivationPath]
?: raise(VisaActivationError.MissingWallet.tangemError)
}
private suspend fun Raise<Throwable>.getSignature(
unlockHotWallet: UnlockHotWallet,
hash: ByteArray,
extendedPublicKey: ExtendedPublicKey,
): String {
val signedHashes = tangemHotSdk.signHashes(
unlockHotWallet = unlockHotWallet,
dataToSign = listOf(
DataToSign(
curve = VisaUtilities.curve,
derivationPath = VisaUtilities.customDerivationPath,
hashes = listOf(hash),
),
),
)
val signature = signedHashes
.firstOrNull { it.curve == VisaUtilities.curve }
?.signatures
?.firstOrNull()
?: raise(VisaCardScanError.FailedToSignChallenge.tangemError)
return VisaUtilities.unmarshallSignature(
signature = signature,
hash = hash,
extendedPublicKey = extendedPublicKey,
)
}
private suspend inline fun <Error, T> withUnlockedHotWallet(
hotWallet: UserWallet.Hot,
block: Raise<Error>.(UnlockHotWallet) -> T,
): Either<Error, T> = either {
try {
val unlockHotWallet = hotWalletAccessor.getContextualUnlock(hotWallet.hotWalletId)
?: hotWalletAccessor.unlockContextual(hotWallet.hotWalletId)
block(unlockHotWallet)
} finally {
hotWalletAccessor.clearContextualUnlock(hotWallet.hotWalletId)
}
}
}

View file

@ -80,9 +80,8 @@ internal interface TangemPayDataModule {
deviceSecurity: DeviceSecurityInfoProvider,
): TangemPayMainScreenCustomerInfoUseCase {
return TangemPayMainScreenCustomerInfoUseCase(
repository = repository,
onboardingRepository = repository,
customerOrderRepository = customerOrderRepository,
tangemPayOnboardingRepository = tangemPayOnboardingRepository,
eligibilityManager = eligibilityManager,
deviceSecurity = deviceSecurity,
)

View file

@ -65,15 +65,16 @@ internal class DefaultOnboardingRepository @Inject constructor(
override suspend fun produceInitialData(userWalletId: UserWalletId) {
withContext(dispatcherProvider.io) {
val initialCredentials = authDataSource.produceInitialCredentials(cardId = getCardId(userWalletId))
val userWallet = getUserWallet(userWalletId)
val initialCredentials = authDataSource.produceInitialCredentials(userWallet)
.fold(
ifLeft = { error -> error("Can not produce initial data: ${error.message}") },
ifRight = { it },
)
// should storeCheckCustomerWalletResult because we already know this
tangemPayStorage.storeCheckCustomerWalletResult(userWalletId, true)
tangemPayStorage.storeCheckCustomerWalletResult(userWallet.walletId, true)
tangemPayStorage.storeCustomerWalletAddress(
userWalletId = userWalletId,
userWalletId = userWallet.walletId,
customerWalletAddress = initialCredentials.customerWalletAddress,
)
tangemPayStorage.storeAuthTokens(
@ -120,17 +121,13 @@ internal class DefaultOnboardingRepository @Inject constructor(
}
}
private fun getCardId(userWalletId: UserWalletId): String {
private fun getUserWallet(userWalletId: UserWalletId): UserWallet {
val userWallet = if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId }
} else {
userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == userWalletId }
} ?: error("no userWallet found")
return if (userWallet is UserWallet.Cold) {
userWallet.cardId
} else {
TODO("[REDACTED_JIRA]")
}
return userWallet
}
private suspend fun getCustomerInfo(
@ -138,13 +135,13 @@ internal class DefaultOnboardingRepository @Inject constructor(
response: CustomerMeResponse.Result?,
): CustomerInfo {
val card = response?.card
val balance = response?.balance
val fiatBalance = response?.balance?.fiat
val paymentAccount = response?.paymentAccount
val cardInfo = if (paymentAccount != null && card != null && balance != null) {
val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null) {
CardInfo(
lastFourDigits = card.cardNumberEnd,
balance = balance.fiat.availableBalance,
currencyCode = balance.fiat.currency,
balance = fiatBalance.availableBalance,
currencyCode = fiatBalance.currency,
customerWalletAddress = paymentAccount.customerWalletAddress,
depositAddress = response.depositAddress,
)

View file

@ -52,20 +52,29 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
private val storePollingMutex = Mutex()
override suspend fun getCardBalance(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardBalance> {
return requestHelper.runWithErrorLogs(TAG) {
val result = requestHelper.request(userWalletId) { authHeader ->
tangemPayApi.getCardBalance(authHeader)
}.result ?: error("Cannot get card balance")
TangemPayCardBalance(
fiatBalance = result.fiat.availableBalance,
currencyCode = result.fiat.currency,
cryptoBalance = result.crypto.balance,
availableForWithdrawal = result.availableForWithdrawal.amount,
chainId = result.crypto.chainId,
depositAddress = result.crypto.depositAddress,
contractAddress = result.crypto.tokenContractAddress,
)
}
return catch(
block = {
val response = requestHelper.performRequest(userWalletId) { authHeader ->
tangemPayApi.getCardBalance(authHeader)
}.getOrNull()
val fiatBalance = requireNotNull(response?.result?.fiat) { "Cannot get card balance fiat" }
val cryptoBalance = requireNotNull(response.result?.crypto) { "Cannot get card balance crypto" }
val withdrawalAmount = requireNotNull(response.result?.availableForWithdrawal) {
"Cannot get card balance availableForWithdrawal"
}
TangemPayCardBalance(
fiatBalance = fiatBalance.availableBalance,
currencyCode = fiatBalance.currency,
cryptoBalance = cryptoBalance.balance,
availableForWithdrawal = withdrawalAmount.amount,
chainId = cryptoBalance.chainId,
depositAddress = cryptoBalance.depositAddress,
contractAddress = cryptoBalance.tokenContractAddress,
).right()
},
catch = ::catchException,
)
}
override suspend fun revealCardDetails(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardDetails> {
@ -100,7 +109,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
expirationMonth = result.expirationMonth,
).right()
},
catch = { errorConverter.convert(it).left() },
catch = ::catchException,
)
}
@ -286,6 +295,11 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
}
}
private fun <T> catchException(throwable: Throwable): Either<UniversalError, T> {
Timber.tag(TAG).e(throwable)
return errorConverter.convert(throwable).left()
}
private companion object {
const val MAX_POLLING_RETRIES = 3
}

View file

@ -1,23 +1,20 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.left
import com.tangem.core.error.UniversalError
import com.tangem.data.common.quote.QuotesFetcher
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.pay.models.request.WithdrawDataRequest
import com.tangem.datasource.api.pay.models.request.WithdrawRequest
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.WithdrawalResult
import com.tangem.domain.pay.WithdrawalSignatureResult
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.pay.model.WithdrawalSignatureResult
import com.tangem.domain.pay.repository.TangemPaySwapRepository
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.utils.extensions.addHexPrefix
import java.math.BigDecimal
import java.math.RoundingMode
@ -30,29 +27,25 @@ internal class DefaultTangemPaySwapRepository @Inject constructor(
private val tangemPayApi: TangemPayApi,
private val requestHelper: TangemPayRequestPerformer,
private val authDataSource: TangemPayAuthDataSource,
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val quotesFetcher: QuotesFetcher,
private val tangemPayStorage: TangemPayStorage,
) : TangemPaySwapRepository {
override suspend fun withdraw(
userWalletId: UserWalletId,
userWallet: UserWallet,
receiverAddress: String,
cryptoAmount: BigDecimal,
cryptoCurrencyId: CryptoCurrency.RawID,
): Either<UniversalError, WithdrawalResult> {
val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId)
if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError)
return requestHelper.performRequest(userWalletId) { authHeader ->
return requestHelper.performRequest(userWallet.walletId) { authHeader ->
val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress)
tangemPayApi.getWithdrawData(authHeader = authHeader, body = request)
}.map { data ->
val result = data.result
if (result == null) return Either.Left(VisaApiError.WithdrawalDataError)
val result = data.result ?: return VisaApiError.WithdrawalDataError.left()
val signatureResult = authDataSource.getWithdrawalSignature(
cardId = getCardId(userWalletId),
userWallet = userWallet,
hash = result.hash,
).getOrNull()
@ -61,7 +54,7 @@ internal class DefaultTangemPaySwapRepository @Inject constructor(
Either.Right(WithdrawalResult.Cancelled)
}
is WithdrawalSignatureResult.Success -> {
requestHelper.performRequest(userWalletId) { authHeader ->
requestHelper.performRequest(userWallet.walletId) { authHeader ->
val request = WithdrawRequest(
amountInCents = amountInCents,
recipientAddress = receiverAddress,
@ -74,7 +67,7 @@ internal class DefaultTangemPaySwapRepository @Inject constructor(
.mapLeft { return Either.Left(VisaApiError.WithdrawError) }
.map { response ->
val orderId = response.result?.orderId
if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWalletId, orderId)
if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWallet.walletId, orderId)
WithdrawalResult.Success
}
}
@ -103,17 +96,4 @@ internal class DefaultTangemPaySwapRepository @Inject constructor(
return quotes?.quotes[cryptoCurrencyId.value]?.price
}
private fun getCardId(userWalletId: UserWalletId): String {
val userWallet = if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId }
} else {
userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == userWalletId }
} ?: error("No User Wallet found")
return if (userWallet is UserWallet.Cold) {
userWallet.cardId
} else {
TODO("[REDACTED_JIRA]")
}
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.data.pay.usecase
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.WithdrawalResult
import com.tangem.domain.pay.repository.TangemPaySwapRepository
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
@ -15,13 +15,13 @@ internal class DefaultTangemPayWithdrawUseCase @Inject constructor(
) : TangemPayWithdrawUseCase {
override suspend fun invoke(
userWalletId: UserWalletId,
userWallet: UserWallet,
cryptoAmount: BigDecimal,
cryptoCurrencyId: CryptoCurrency.RawID,
receiverCexAddress: String,
): Either<UniversalError, WithdrawalResult> {
return repository.withdraw(
userWalletId = userWalletId,
userWallet = userWallet,
cryptoAmount = cryptoAmount,
receiverAddress = receiverCexAddress,
cryptoCurrencyId = cryptoCurrencyId,

View file

@ -9,6 +9,7 @@ import com.tangem.datasource.api.pay.models.request.GenerateNonceByCustomerWalle
import com.tangem.datasource.api.pay.models.request.GetTokenByCustomerWalletRequest
import com.tangem.datasource.api.pay.models.response.TangemPayErrorResponse
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.domain.card.common.visa.VisaUtilities
import com.tangem.domain.visa.datasource.TangemPayRemoteDataSource
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.TangemPayAuthTokens
@ -56,7 +57,7 @@ internal class DefaultTangemPayRemoteDataSource @Inject constructor(
authType = "customer_wallet",
sessionId = sessionId,
signature = signature,
messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce",
messageFormat = VisaUtilities.signWithNonceMessage(nonce),
),
).getOrThrow()
}.map { response ->

View file

@ -83,6 +83,7 @@ internal class WcEthMessageSignUseCase @AssistedInject constructor(
object LegacySdkHelper {
private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
// TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK
fun prepareToSendMessageData(signedHash: ByteArray, hashToSign: ByteArray, walletManager: WalletManager): String =
UnmarshalHelper.unmarshalSignatureExtended(
signature = signedHash,

View file

@ -1,9 +1,15 @@
package com.tangem.domain.card.common.visa
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.UnmarshalHelper
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
private const val VISA_BATCH_START = "AE"
@ -11,15 +17,16 @@ private const val VISA_BATCH_START_2 = "FFFC"
object VisaUtilities {
const val tokenId = "tether"
val visaBlockchain = Blockchain.Polygon
val visaDefaultDerivationPath
get() = visaBlockchain.derivationPath(DerivationStyle.V3)
val customDerivationPath = DerivationPath("m/44'/60'/999999'/0/0")
val curve = EllipticCurve.Secp256k1
fun visaDefaultDerivationPath(style: DerivationStyle) = visaBlockchain.derivationPath(style)
fun signWithNonceMessage(nonce: String): String {
return "Tangem Pay wants to sign in with your account. Nonce: $nonce"
}
fun isVisaCard(card: CardDTO): Boolean {
return isVisaCard(card.firmwareVersion.doubleValue, card.batchId)
@ -29,4 +36,26 @@ object VisaUtilities {
return firmwareVersion in FirmwareVersion.visaRange &&
(batchId.startsWith(VISA_BATCH_START) || batchId.startsWith(VISA_BATCH_START_2))
}
// TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK
fun hashPersonalMessage(message: ByteArray): ByteArray {
val prefix = "\u0019Ethereum Signed Message:\n${message.size}".toByteArray()
return (prefix + message).toKeccak()
}
fun generateAddressFromExtendedKey(extendedPublicKey: ExtendedPublicKey): String {
val derivationData = visaBlockchain.makeAddressesFromExtendedPublicKey(
extendedPublicKey = extendedPublicKey,
cachedIndex = null,
)
return derivationData.address
}
fun unmarshallSignature(signature: ByteArray, hash: ByteArray, extendedPublicKey: ExtendedPublicKey): String {
return UnmarshalHelper.unmarshalSignatureExtended(
signature = signature,
hash = hash,
publicKey = extendedPublicKey.publicKey.toDecompressedPublicKey(),
).asRSVLegacyEVM().toHexString().lowercase()
}
}

View file

@ -28,6 +28,7 @@ enum class NetworkType {
BAND_PROTOCOL,
BITSONG,
CANTO,
CARDANO,
CHIHUAHUA,
COMDEX,
COREUM,

View file

@ -56,4 +56,8 @@ interface SettingsRepository {
suspend fun setGooglePayAvailability(value: Boolean)
suspend fun isGooglePayAvailability(): Boolean
suspend fun isRootDetectedWarningShown(): Boolean
suspend fun setRootDetectedWarningShown(value: Boolean)
}

View file

@ -1,58 +0,0 @@
package com.tangem.domain.staking.usecase
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.toStakingTarget
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.repositories.StakeKitRepository
import com.tangem.domain.staking.toggles.StakingFeatureToggles
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
/**
* Emits a map of StakingTarget values per currency for staking.
*
* Return map:
* - key: currency staking key (coinGeckoId + "_" + symbol)
* - value: list of staking targets (validators or vaults)
*/
class StakingApyFlowUseCase(
private val stakeKitRepository: StakeKitRepository,
private val p2pEthPoolRepository: P2PEthPoolRepository,
private val stakingFeatureToggles: StakingFeatureToggles,
) {
operator fun invoke(): Flow<Map<String, List<StakingTarget>>> {
return combine(
stakeKitRepository.getEnabledYields(),
p2pEthPoolRepository.getVaultsFlow(),
) { yields, p2pVaults ->
val stakeKitMap = yields.filterNot { yield ->
val coinGeckoId = yield.token.coinGeckoId
val isCardanoYield = coinGeckoId == Blockchain.Cardano.toCoinId()
val isEthYield = coinGeckoId == Blockchain.Ethereum.toCoinId()
when {
isCardanoYield && !stakingFeatureToggles.isCardanoStakingEnabled -> true
isEthYield && !stakingFeatureToggles.isEthStakingEnabled -> true
else -> false
}
}.associate { yield ->
val key = "${yield.token.coinGeckoId}_${yield.token.symbol}"
val targets = yield.validators.map { it.toStakingTarget() }
key to targets
}
val p2pMap = if (p2pVaults.isNotEmpty() && stakingFeatureToggles.isEthStakingEnabled) {
val ethKey = "${Blockchain.Ethereum.toCoinId()}_${Blockchain.Ethereum.currency}"
val targets = p2pVaults.map { it.toStakingTarget() }
mapOf(ethKey to targets)
} else {
emptyMap()
}
stakeKitMap + p2pMap
}
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.domain.staking.usecase
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.repositories.StakingRepository
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
/**
* Returns staking availability for a list of crypto currencies for a specific user wallet
*
* Return map:
* - key: crypto currency
* - value: staking availability for the currency
*/
class StakingAvailabilityListUseCase(
private val stakingRepository: StakingRepository,
) {
suspend fun invokeSync(
userWalletId: UserWalletId,
cryptoCurrencyList: List<CryptoCurrency>,
): Map<CryptoCurrency, StakingAvailability> {
return coroutineScope {
cryptoCurrencyList.map { cryptoCurrency ->
async {
cryptoCurrency to stakingRepository.getStakingAvailabilitySync(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
)
}
}.awaitAll().toMap()
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.domain.pay.model
package com.tangem.domain.pay
sealed class WithdrawalSignatureResult {

View file

@ -1,12 +1,16 @@
package com.tangem.domain.pay.datasource
import arrow.core.Either
import com.tangem.domain.pay.model.WithdrawalSignatureResult
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.WithdrawalSignatureResult
import com.tangem.domain.visa.model.TangemPayInitialCredentials
interface TangemPayAuthDataSource {
suspend fun produceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials>
suspend fun produceInitialCredentials(userWallet: UserWallet): Either<Throwable, TangemPayInitialCredentials>
suspend fun getWithdrawalSignature(cardId: String, hash: String): Either<Throwable, WithdrawalSignatureResult>
suspend fun getWithdrawalSignature(
userWallet: UserWallet,
hash: String,
): Either<Throwable, WithdrawalSignatureResult>
}

View file

@ -3,14 +3,14 @@ package com.tangem.domain.pay.repository
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.WithdrawalResult
import java.math.BigDecimal
interface TangemPaySwapRepository {
suspend fun withdraw(
userWalletId: UserWalletId,
userWallet: UserWallet,
receiverAddress: String,
cryptoAmount: BigDecimal,
cryptoCurrencyId: CryptoCurrency.RawID,

View file

@ -21,9 +21,8 @@ private const val TAG = "TangemPayMainScreenCustomerInfoUseCase"
* Works only if the user already authorised at least once (won't emit anything otherwise)
*/
class TangemPayMainScreenCustomerInfoUseCase(
private val repository: OnboardingRepository,
private val onboardingRepository: OnboardingRepository,
private val customerOrderRepository: CustomerOrderRepository,
private val tangemPayOnboardingRepository: OnboardingRepository,
private val eligibilityManager: TangemPayEligibilityManager,
private val deviceSecurity: DeviceSecurityInfoProvider,
) {
@ -32,7 +31,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
field = MutableStateFlow(value = mapOf())
suspend fun fetch(userWalletId: UserWalletId) {
Timber.tag(TAG).i("fetch: $userWalletId")
Timber.tag(TAG).i("fetch: ${userWalletId.stringValue}")
if (deviceSecurity.isSecurityExposed()) {
Timber.tag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}")
@ -43,7 +42,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
return // fast exit
}
repository.checkCustomerWallet(userWalletId)
onboardingRepository.checkCustomerWallet(userWalletId)
.fold(
ifLeft = { error ->
Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}")
@ -64,7 +63,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
// if there's no tangem pay, check eligibility and show onboarding banner
val isEligible = eligibilityManager.getEligibleWallets().any { it.walletId == userWalletId }
if (isEligible) {
if (tangemPayOnboardingRepository.getHideMainOnboardingBanner(userWalletId)) {
if (onboardingRepository.getHideMainOnboardingBanner(userWalletId)) {
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
} else {
updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right())
@ -95,10 +94,10 @@ class TangemPayMainScreenCustomerInfoUseCase(
private suspend fun proceedWithPaeraCustomerResult(
userWalletId: UserWalletId,
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
if (!tangemPayOnboardingRepository.isTangemPayInitialDataProduced(userWalletId)) {
if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) {
return TangemPayCustomerInfoError.RefreshNeededError.left()
}
val orderId = repository.getOrderId(userWalletId)
val orderId = onboardingRepository.getOrderId(userWalletId)
return if (orderId != null) {
proceedWithOrderId(userWalletId = userWalletId, orderId = orderId)
} else {
@ -109,7 +108,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
private suspend fun proceedWithoutOrder(
userWalletId: UserWalletId,
): Either<TangemPayCustomerInfoError, MainScreenCustomerInfo> {
return repository.getCustomerInfo(userWalletId)
return onboardingRepository.getCustomerInfo(userWalletId)
.mapLeft { error ->
Timber.tag(TAG).e("mapErrorForCustomer: $error")
error.mapErrorForCustomer()
@ -118,7 +117,7 @@ class TangemPayMainScreenCustomerInfoUseCase(
Timber.tag(TAG).i("customerInfo")
if (customerInfo.cardInfo == null && customerInfo.isKycApproved) {
// If order id wasn't saved -> start order creation and get customer info
repository.createOrder(userWalletId)
onboardingRepository.createOrder(userWalletId)
}
MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.UNKNOWN)
}
@ -148,10 +147,10 @@ class TangemPayMainScreenCustomerInfoUseCase(
OrderStatus.CANCELED,
OrderStatus.UNKNOWN,
-> {
repository.clearOrderId(userWalletId)
onboardingRepository.clearOrderId(userWalletId)
// If order was cancelled -> start order creation
if (orderStatus == OrderStatus.CANCELED) repository.createOrder(userWalletId)
repository.getCustomerInfo(userWalletId = userWalletId)
if (orderStatus == OrderStatus.CANCELED) onboardingRepository.createOrder(userWalletId)
onboardingRepository.getCustomerInfo(userWalletId = userWalletId)
.mapLeft { it.mapErrorForCustomer() }
.map { customerInfo ->
MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus)

View file

@ -3,14 +3,14 @@ package com.tangem.domain.tangempay
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.WithdrawalResult
import java.math.BigDecimal
interface TangemPayWithdrawUseCase {
suspend operator fun invoke(
userWalletId: UserWalletId,
userWallet: UserWallet,
cryptoAmount: BigDecimal,
cryptoCurrencyId: CryptoCurrency.RawID,
receiverCexAddress: String,

View file

@ -38,6 +38,7 @@ dependencies {
implementation(projects.domain.feedback)
implementation(projects.domain.feedback.models)
implementation(projects.domain.hotWallet)
implementation(projects.domain.notifications)
/** Common */
implementation(projects.common.ui)

View file

@ -149,7 +149,6 @@ internal class AccessCodeModel @Inject constructor(
onClick = {
uiState.update { currentState ->
currentState.copy(
accessCode = "",
onAccessCodeChange = ::onAccessCodeChange,
requestFocus = triggeredEvent(Unit, ::consumeRequestFocusEvent),
)

View file

@ -15,8 +15,8 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
@ -25,7 +25,6 @@ import com.tangem.features.hotwallet.accesscode.AccessCodeComponent
import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.MutableStateFlow
@ -37,7 +36,7 @@ import javax.inject.Inject
internal class AddExistingWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
private val notificationsRepository: NotificationsRepository,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val trackingContextProxy: TrackingContextProxy,
private val setAccessCodeSkippedUseCase: SetAccessCodeSkippedUseCase,
@ -79,8 +78,8 @@ internal class AddExistingWalletModel @Inject constructor(
private fun navigateToPushNotificationsOrNext() {
modelScope.launch {
val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION)
if (shouldRequestPush) {
val shouldAskNotificationPermissions = notificationsRepository.shouldAskNotificationPermissionsViaBs()
if (shouldAskNotificationPermissions) {
stackNavigation.replaceAll(AddExistingWalletRoute.PushNotifications)
} else {
stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished)

View file

@ -18,8 +18,8 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents
import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
@ -28,7 +28,6 @@ import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartCompone
import com.tangem.features.hotwallet.accesscode.AccessCodeComponent
import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent
import com.tangem.features.hotwallet.walletactivation.entry.routing.WalletActivationRoute
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.hotwallet.WalletActivationComponent
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
@ -44,7 +43,7 @@ internal class WalletActivationModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
private val notificationsRepository: NotificationsRepository,
private val setAccessCodeSkippedUseCase: SetAccessCodeSkippedUseCase,
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val trackingContextProxy: TrackingContextProxy,
@ -118,8 +117,8 @@ internal class WalletActivationModel @Inject constructor(
private fun navigateToPushNotificationsOrNext() {
modelScope.launch {
val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION)
if (shouldRequestPush) {
val shouldAskNotificationPermissions = notificationsRepository.shouldAskNotificationPermissionsViaBs()
if (shouldAskNotificationPermissions) {
stackNavigation.replaceAll(WalletActivationRoute.PushNotifications)
} else {
stackNavigation.replaceAll(WalletActivationRoute.SetupFinished)

View file

@ -45,17 +45,12 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.staking.*
import com.tangem.domain.staking.analytics.StakeScreenSource
import com.tangem.domain.staking.analytics.StakingAnalyticsEvent
import com.tangem.domain.staking.model.P2PEthPoolIntegration
import com.tangem.domain.staking.model.StakeKitIntegration
import com.tangem.domain.staking.model.StakingApproval
import com.tangem.domain.staking.model.StakingIntegration
import com.tangem.domain.staking.model.StakingIntegrationID
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.*
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
import com.tangem.domain.staking.utils.getValidatorsCount
import com.tangem.domain.tokens.*
import com.tangem.domain.transaction.error.GetFeeError
@ -97,6 +92,7 @@ import com.tangem.utils.extensions.isSingleItem
import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -178,6 +174,7 @@ internal class StakingModel @Inject constructor(
StakeKitIntegration(integrationId, yield)
}
StakingIntegrationID.P2PEthPool -> {
// TODO p2p avoid network call
val vaults = p2pEthPoolRepository.getVaults().getOrElse { emptyList() }
P2PEthPoolIntegration(integrationId, vaults)
}
@ -615,10 +612,23 @@ internal class StakingModel @Inject constructor(
override fun onActiveStake(activeStake: BalanceState) {
val networkId = cryptoCurrencyStatus.currency.network.rawId
if (isSingleAction(networkId, activeStake)) {
val preferredValidators = (integration as? StakeKitIntegration)?.targets
?.filterIsInstance<StakingTarget.Validator>()
?.filter { it.delegate.preferred }
.orEmpty()
val pendingActions = activeStake.pendingActions.mapNotNull { action ->
if (action.type in listOf(StakingActionType.RESTAKE, StakingActionType.STAKE) &&
preferredValidators.isSingleItem()
) {
null
} else {
action
}
}.toImmutableList()
if (isSingleAction(networkId, pendingActions)) {
prepareForConfirmation(
balanceType = activeStake.type,
pendingActions = activeStake.pendingActions,
pendingActions = pendingActions,
balanceState = activeStake,
target = activeStake.target,
amountValue = activeStake.cryptoValue,
@ -627,7 +637,7 @@ internal class StakingModel @Inject constructor(
} else {
stateController.update(
ShowActionSelectorBottomSheetTransformer(
pendingActions = withStubUnstakeAction(networkId, activeStake),
pendingActions = withStubUnstakeAction(networkId, pendingActions, activeStake),
onActionSelect = { action ->
prepareForConfirmation(
balanceType = activeStake.type,

View file

@ -37,17 +37,21 @@ internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (t
null -> TextReference.EMPTY
}
internal fun isSingleAction(networkId: String, activeStake: BalanceState): Boolean {
val isSingleAction = activeStake.pendingActions.size <= 1 // Either single or none pending actions
val isCompositePendingActions = isCompositePendingActions(networkId, activeStake.pendingActions)
val isRestake = activeStake.pendingActions.any { it.type.isRestake }
internal fun isSingleAction(networkId: String, pendingActions: List<PendingAction>): Boolean {
val isSingleAction = pendingActions.size <= 1 // Either single or none pending actions
val isCompositePendingActions = isCompositePendingActions(networkId, pendingActions.toPersistentList())
val isRestake = pendingActions.any { it.type.isRestake }
return isSingleAction && !isRestake || isCompositePendingActions
}
internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState): ImmutableList<PendingAction> {
internal fun withStubUnstakeAction(
networkId: String,
pendingActions: List<PendingAction>,
activeStake: BalanceState,
): ImmutableList<PendingAction> {
return if (isStubUnstakeAction(networkId) && activeStake.type != BalanceType.REWARDS) {
activeStake.pendingActions.plus(
pendingActions.plus(
PendingAction(
type = StakingActionType.UNSTAKE,
passthrough = "",

View file

@ -845,7 +845,7 @@ internal class SwapModel @Inject constructor(
private suspend fun processTangemPayWithdrawal(swapTransactionState: SwapTransactionState.TangemPayWithdrawalData) {
tangemPayWithdrawUseCase(
userWalletId = userWalletId,
userWallet = userWallet,
cryptoAmount = swapTransactionState.cryptoAmount,
cryptoCurrencyId = swapTransactionState.cryptoCurrencyId,
receiverCexAddress = swapTransactionState.cexAddress,

View file

@ -41,7 +41,6 @@ internal sealed class TangemPayDetailsBalanceBlockState {
data class Content(
override val actionButtons: ImmutableList<ActionButtonConfig>,
val cryptoBalance: String,
val fiatBalance: String,
val isBalanceFlickering: Boolean,
) : TangemPayDetailsBalanceBlockState()

View file

@ -2,10 +2,8 @@ package com.tangem.features.tangempay.model.transformers
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.domain.pay.model.TangemPayCardBalance
@ -13,7 +11,6 @@ import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
import java.util.Currency
internal class DetailsBalanceTransformer(
@ -37,7 +34,6 @@ internal class DetailsBalanceTransformer(
TangemPayDetailsBalanceBlockState.Content(
isBalanceFlickering = false,
fiatBalance = getFiatBalanceText(balance.value),
cryptoBalance = getCryptoBalanceText(balance.value.cryptoBalance, cryptoCurrency),
actionButtons = prevState.balanceBlockState.actionButtons,
)
}
@ -52,8 +48,4 @@ internal class DetailsBalanceTransformer(
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
}
}
private fun getCryptoBalanceText(cryptoBalance: BigDecimal, cryptoCurrency: CryptoCurrency): String {
return cryptoBalance.format { crypto(cryptoCurrency = cryptoCurrency) }
}
}

View file

@ -175,11 +175,6 @@ private fun TangemPayDetailsBalanceBlock(
state = state,
isBalanceHidden = isBalanceHidden,
)
CryptoBalance(
modifier = Modifier.padding(start = 12.dp, top = 4.dp),
state = state,
isBalanceHidden = isBalanceHidden,
)
if (state.actionButtons.isNotEmpty()) {
HorizontalActionChips(
modifier = Modifier.padding(top = 12.dp),
@ -220,37 +215,6 @@ private fun FiatBalance(
)
}
}
@Suppress("UnusedPrivateMember")
@Composable
private fun CryptoBalance(
state: TangemPayDetailsBalanceBlockState,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (state) {
is TangemPayDetailsBalanceBlockState.Loading -> RectangleShimmer(
modifier = modifier.size(
width = TangemTheme.dimens.size70,
height = TangemTheme.dimens.size16,
),
)
is TangemPayDetailsBalanceBlockState.Content -> Text(
modifier = modifier,
text = state.cryptoBalance.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.caption2.applyBladeBrush(
isEnabled = state.isBalanceFlickering,
textColor = TangemTheme.colors.text.tertiary,
),
)
is TangemPayDetailsBalanceBlockState.Error -> Text(
modifier = modifier,
text = DASH_SIGN.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
// endregion
@OptIn(ExperimentalMaterial3Api::class)
@ -345,7 +309,6 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
onClick = {},
),
),
cryptoBalance = "1234.56 USDT",
fiatBalance = "$1234.56",
isBalanceFlickering = false,
),

View file

@ -89,9 +89,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
private fun checkCustomerInfo(userWalletId: UserWalletId) {
modelScope.launch {
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true))
repository.getCustomerInfo(
userWalletId = userWalletId,
)
repository.getCustomerInfo(userWalletId = userWalletId)
.onRight { customerInfo ->
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false))
when {
@ -159,21 +157,20 @@ internal class TangemPayOnboardingModel @Inject constructor(
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false))
return@launch
}
repository.getCustomerInfo(
userWalletId = userWalletId,
).fold(
ifLeft = { error ->
Timber.e("Error getCustomerInfo: ${error.errorCode}")
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false))
},
ifRight = { customerInfo ->
if (customerInfo.isKycApproved) {
back()
} else {
openKyc(userWalletId)
}
},
)
repository.getCustomerInfo(userWalletId = userWalletId)
.fold(
ifLeft = { error ->
Timber.e("Error getCustomerInfo: ${error.errorCode}")
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false))
},
ifRight = { customerInfo ->
if (customerInfo.isKycApproved) {
back()
} else {
openKyc(userWalletId)
}
},
)
}
}

View file

@ -5,7 +5,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.nft.GetNFTCollectionsUseCase
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.repository.WalletsRepository
@ -45,7 +45,7 @@ internal class MultiWalletContentLoader(
private val walletsRepository: WalletsRepository,
private val currenciesRepository: CurrenciesRepository,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
@ -64,7 +64,7 @@ internal class MultiWalletContentLoader(
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
stakingAvailabilityListUseCase = stakingAvailabilityListUseCase,
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
).let(::add)

View file

@ -5,7 +5,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.nft.GetNFTCollectionsUseCase
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.repository.WalletsRepository
@ -44,7 +44,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase,
private val currenciesRepository: CurrenciesRepository,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
@ -70,7 +70,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
getNFTCollectionsUseCase = getNFTCollectionsUseCase,
currenciesRepository = currenciesRepository,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
stakingAvailabilityListUseCase = stakingAvailabilityListUseCase,
hotWalletFeatureToggles = hotWalletFeatureToggles,
tangemPayFeatureToggles = tangemPayFeatureToggles,
tangemPayMainSubscriberFactory = tangemPayMainSubscriberFactory,

View file

@ -3,7 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase
@ -34,7 +34,7 @@ internal class SingleWalletWithTokenContentLoader(
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val getStoryContentUseCase: GetStoryContentUseCase,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
) : WalletContentLoader(id = userWallet.walletId) {
@ -50,7 +50,7 @@ internal class SingleWalletWithTokenContentLoader(
tokenListStore = tokenListStore,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
stakingAvailabilityListUseCase = stakingAvailabilityListUseCase,
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
).let(::add)
MultiWalletWarningsSubscriber(

View file

@ -4,7 +4,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase
@ -35,7 +35,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val getStoryContentUseCase: GetStoryContentUseCase,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
) {
@ -55,7 +55,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
getStoryContentUseCase = getStoryContentUseCase,
shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
stakingAvailabilityListUseCase = stakingAvailabilityListUseCase,
hotWalletFeatureToggles = hotWalletFeatureToggles,
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
)

View file

@ -1,8 +1,9 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
@ -19,7 +20,7 @@ internal class SetTokenListTransformer(
private val appCurrency: AppCurrency,
private val clickIntents: WalletClickIntents,
private val yieldSupplyApyMap: Map<String, BigDecimal> = emptyMap(),
private val stakingApyMap: Map<String, List<StakingTarget>> = emptyMap(),
private val stakingAvailabilityMap: Map<CryptoCurrency, StakingAvailability> = emptyMap(),
private val shouldShowMainPromo: Boolean,
) : WalletStateTransformer(userWallet.walletId) {
@ -64,7 +65,7 @@ internal class SetTokenListTransformer(
appCurrency = appCurrency,
clickIntents = clickIntents,
yieldModuleApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
).convert(value = this)
}

View file

@ -14,11 +14,12 @@ import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
@ -38,7 +39,7 @@ internal class TokenListStateConverter(
private val selectedWallet: UserWallet,
private val clickIntents: WalletClickIntents,
private val yieldModuleApyMap: Map<String, BigDecimal>,
private val stakingApyMap: Map<String, List<StakingTarget>>,
private val stakingAvailabilityMap: Map<CryptoCurrency, StakingAvailability>,
private val shouldShowMainPromo: Boolean,
) : Converter<WalletTokensListState, WalletTokensListState> {
@ -72,7 +73,7 @@ internal class TokenListStateConverter(
appCurrency = appCurrency,
yieldModuleApyMap = yieldModuleApyMap,
yieldSupplyPromoBannerKey = yieldSupplyPromoBannerKeyConverter.convert(params),
stakingApyMap = stakingApyMap,
stakingApyMap = stakingAvailabilityMap,
onItemClick = { _, status -> onTokenClick(accountId, status) },
onItemLongClick = { _, status -> onTokenLongClick(accountId, status) },
onApyLabelClick = { status, apySource, apy -> onApyLabelClick(status, apySource, apy) },

View file

@ -1,15 +1,15 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.utils.coroutines.combine7
import com.tangem.utils.coroutines.combine6
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -31,29 +31,36 @@ internal class AccountListSubscriber @AssistedInject constructor(
override val stateController: WalletStateController,
override val clickIntents: WalletClickIntents,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
) : BasicAccountListSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<*> = combine7(
override fun create(coroutineScope: CoroutineScope): Flow<*> = combine6(
flow1 = getAccountStatusListFlow(),
flow2 = getAppCurrencyFlow(),
flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet),
flow4 = accountDependencies.isAccountsModeEnabledUseCase(),
flow5 = yieldSupplyApyFlow(),
flow6 = stakingApyFlow(),
flow7 = yieldSupplyGetShouldShowMainPromoFlow(),
transform = ::updateState,
)
flow6 = yieldSupplyGetShouldShowMainPromoFlow(),
) { accountList, appCurrency, expandedAccounts, isAccountMode, yieldSupplyApyMap, shouldShowMainPromo ->
updateState(
accountList = accountList,
appCurrency = appCurrency,
expandedAccounts = expandedAccounts,
isAccountMode = isAccountMode,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityListUseCase.invokeSync(
userWalletId = userWallet.walletId,
cryptoCurrencyList = accountList.flattenCurrencies().map(CryptoCurrencyStatus::currency),
),
shouldShowMainPromo = shouldShowMainPromo,
)
}
private fun yieldSupplyApyFlow(): Flow<Map<String, BigDecimal>> {
return yieldSupplyApyFlowUseCase().distinctUntilChanged()
}
private fun stakingApyFlow(): Flow<Map<String, List<StakingTarget>>> {
return stakingApyFlowUseCase().distinctUntilChanged()
}
private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow<Boolean> {
return yieldSupplyGetShouldShowMainPromoUseCase().distinctUntilChanged()
}

View file

@ -8,8 +8,9 @@ import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.account.AccountDependencies
@ -48,7 +49,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
expandedAccounts: Set<AccountId>,
isAccountMode: Boolean,
yieldSupplyApyMap: Map<String, BigDecimal> = emptyMap(),
stakingApyMap: Map<String, List<StakingTarget>> = emptyMap(),
stakingAvailabilityMap: Map<CryptoCurrency, StakingAvailability> = emptyMap(),
shouldShowMainPromo: Boolean = false,
) {
val mainAccount = accountList.mainAccount
@ -67,7 +68,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
appCurrency = appCurrency,
portfolioId = PortfolioId(mainAccount.accountId),
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
)
}
@ -77,7 +78,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
params = convertParams,
appCurrency = appCurrency,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
)
}
@ -89,7 +90,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
appCurrency: AppCurrency,
portfolioId: PortfolioId,
yieldSupplyApyMap: Map<String, BigDecimal> = emptyMap(),
stakingApyMap: Map<String, List<StakingTarget>> = emptyMap(),
stakingAvailabilityMap: Map<CryptoCurrency, StakingAvailability> = emptyMap(),
shouldShowMainPromo: Boolean,
) {
val tokenList = maybeTokenList.getOrElse(
@ -119,7 +120,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
params = TokenConverterParams.Wallet(portfolioId, tokenList),
appCurrency = appCurrency,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
)
}
@ -128,7 +129,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
params: TokenConverterParams,
appCurrency: AppCurrency,
yieldSupplyApyMap: Map<String, BigDecimal> = emptyMap(),
stakingApyMap: Map<String, List<StakingTarget>> = emptyMap(),
stakingAvailabilityMap: Map<CryptoCurrency, StakingAvailability> = emptyMap(),
shouldShowMainPromo: Boolean,
) {
stateController.update(
@ -138,7 +139,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
appCurrency = appCurrency,
clickIntents = clickIntents,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
),
)

View file

@ -8,11 +8,12 @@ import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.model.StakingTarget
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase
@ -41,7 +42,7 @@ internal abstract class BasicTokenListSubscriber(
private val walletWithFundsChecker: WalletWithFundsChecker,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingApyFlowUseCase: StakingApyFlowUseCase,
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
) : WalletSubscriber() {
@ -71,9 +72,8 @@ internal abstract class BasicTokenListSubscriber(
},
flow2 = appCurrencyFlow(),
flow3 = yieldSupplyApyFlow(),
flow4 = stakingApyFlow(),
flow5 = yieldSupplyGetShouldShowMainPromoFlow(),
transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, stakingApyMap, shouldShowMainPromo ->
flow4 = yieldSupplyGetShouldShowMainPromoFlow(),
transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, shouldShowMainPromo ->
val tokenList = maybeTokenList.getOrElse(
ifLoading = { maybeContent ->
val isRefreshing = stateHolder.getWalletState(userWallet.walletId)
@ -101,7 +101,10 @@ internal abstract class BasicTokenListSubscriber(
params = TokenConverterParams.Wallet(PortfolioId(userWallet.walletId), tokenList),
appCurrency = appCurrency,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
stakingAvailabilityMap = stakingAvailabilityListUseCase.invokeSync(
userWalletId = userWallet.walletId,
cryptoCurrencyList = tokenList.flattenCurrencies().map(CryptoCurrencyStatus::currency),
),
shouldShowMainPromo = shouldShowMainPromo,
)
@ -128,7 +131,7 @@ internal abstract class BasicTokenListSubscriber(
params: TokenConverterParams,
appCurrency: AppCurrency,
yieldSupplyApyMap: Map<String, BigDecimal>,
stakingApyMap: Map<String, List<StakingTarget>>,
stakingAvailabilityMap: Map<CryptoCurrency, StakingAvailability>,
shouldShowMainPromo: Boolean,
) {
stateHolder.update(
@ -138,7 +141,7 @@ internal abstract class BasicTokenListSubscriber(
appCurrency = appCurrency,
clickIntents = clickIntents,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingApyMap = stakingApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
),
)
@ -156,9 +159,6 @@ internal abstract class BasicTokenListSubscriber(
private fun yieldSupplyApyFlow(): Flow<Map<String, BigDecimal>> = yieldSupplyApyFlowUseCase()
.distinctUntilChanged()
private fun stakingApyFlow(): Flow<Map<String, List<StakingTarget>>> = stakingApyFlowUseCase()
.distinctUntilChanged()
private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow<Boolean> = yieldSupplyGetShouldShowMainPromoUseCase()
.distinctUntilChanged()
}

View file

@ -8,7 +8,7 @@ import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
@ -32,7 +32,7 @@ internal class MultiWalletTokenListSubscriber(
walletWithFundsChecker: WalletWithFundsChecker,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
stakingApyFlowUseCase: StakingApyFlowUseCase,
stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
) : BasicTokenListSubscriber(
userWallet = userWallet,
@ -42,7 +42,7 @@ internal class MultiWalletTokenListSubscriber(
walletWithFundsChecker = walletWithFundsChecker,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
stakingAvailabilityListUseCase = stakingAvailabilityListUseCase,
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
) {

View file

@ -5,7 +5,7 @@ import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.usecase.StakingApyFlowUseCase
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase
@ -27,7 +27,7 @@ internal class SingleWalletWithTokenListSubscriber(
walletWithFundsChecker: WalletWithFundsChecker,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
stakingApyFlowUseCase: StakingApyFlowUseCase,
stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
) : BasicTokenListSubscriber(
userWallet = userWallet,
@ -37,7 +37,7 @@ internal class SingleWalletWithTokenListSubscriber(
walletWithFundsChecker = walletWithFundsChecker,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase,
stakingApyFlowUseCase = stakingApyFlowUseCase,
stakingAvailabilityListUseCase = stakingAvailabilityListUseCase,
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
) {

View file

@ -204,6 +204,17 @@ sealed class YieldSupplyAnalytics(
),
)
data class NoticeHighFee(
val token: String,
val blockchain: String,
) : YieldSupplyAnalytics(
event = "Notice - High Network Fee",
params = mapOf(
TOKEN_PARAM to token,
BLOCKCHAIN to blockchain,
),
)
enum class Action(val value: String) {
Start("Start"),
Approve("Approve"),

View file

@ -78,6 +78,7 @@ internal class YieldSupplyModel @Inject constructor(
var userWallet: UserWallet by Delegates.notNull()
private val fetchCurrencyJobHolder = JobHolder()
private val loadStatusJobHolder = JobHolder()
private var lastStatusCheckTimestamp = 0L
private val isFirstCryptoCurrencyStatusEmission = AtomicBoolean(true)
@ -134,22 +135,20 @@ internal class YieldSupplyModel @Inject constructor(
}
}
private fun loadTokenStatus() {
private suspend fun loadTokenStatus() {
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
modelScope.launch(dispatchers.default) {
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken)
.onRight { tokenStatus ->
uiState.update(
YieldSupplyTokenStatusSuccessTransformer(
tokenStatus = tokenStatus,
onStartEarningClick = ::onStartEarningClick,
),
)
}.onLeft {
Timber.e(it)
uiState.update { YieldSupplyUM.Initial }
}
}
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken)
.onRight { tokenStatus ->
uiState.update(
YieldSupplyTokenStatusSuccessTransformer(
tokenStatus = tokenStatus,
onStartEarningClick = ::onStartEarningClick,
),
)
}.onLeft {
Timber.e(it)
uiState.update { YieldSupplyUM.Initial }
}
}
override fun onStartEarningClick() {
@ -183,7 +182,9 @@ internal class YieldSupplyModel @Inject constructor(
}
@Suppress("MaximumLineLength")
private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch {
private fun onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus: CryptoCurrencyStatus) = modelScope.launch(
dispatchers.default,
) {
val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
val tokenProtocolStatus = yieldSupplyRepository.getTokenProtocolStatus(
userWallet.walletId,
@ -234,7 +235,7 @@ internal class YieldSupplyModel @Inject constructor(
lastStatusCheckTimestamp = 0L
}
}
}
}.saveIn(loadStatusJobHolder)
private fun showProcessing(status: YieldSupplyEnterStatus) {
uiState.update {
@ -246,24 +247,21 @@ internal class YieldSupplyModel @Inject constructor(
fetchCurrencyWithDelay()
}
private fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) {
private suspend fun loadStatus(cryptoCurrencyStatus: CryptoCurrencyStatus) {
val yieldSupplyStatus = cryptoCurrencyStatus.value.yieldSupplyStatus
modelScope
.launch {
yieldSupplyRepository.saveTokenProtocolStatus(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,
yieldSupplyEnterStatus = null,
)
if (yieldSupplyStatus?.isActive == true) {
loadActiveState(
cryptoCurrencyStatus = cryptoCurrencyStatus,
yieldSupplyStatus = yieldSupplyStatus,
)
} else {
loadTokenStatus()
}
}
yieldSupplyRepository.saveTokenProtocolStatus(
userWalletId = userWallet.walletId,
cryptoCurrency = cryptoCurrency,
yieldSupplyEnterStatus = null,
)
if (yieldSupplyStatus?.isActive == true) {
loadActiveState(
cryptoCurrencyStatus = cryptoCurrencyStatus,
yieldSupplyStatus = yieldSupplyStatus,
)
} else {
loadTokenStatus()
}
}
private fun fetchCurrencyWithDelay() {
@ -280,7 +278,10 @@ internal class YieldSupplyModel @Inject constructor(
}.saveIn(fetchCurrencyJobHolder)
}
private fun loadActiveState(cryptoCurrencyStatus: CryptoCurrencyStatus, yieldSupplyStatus: YieldSupplyStatus) {
private suspend fun loadActiveState(
cryptoCurrencyStatus: CryptoCurrencyStatus,
yieldSupplyStatus: YieldSupplyStatus,
) {
val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return
val showWarningIcon = !yieldSupplyStatus.isAllowedToSpend
val isShowInfoIconPrevState = when (val state = uiState.value) {
@ -295,50 +296,48 @@ internal class YieldSupplyModel @Inject constructor(
),
)
}
modelScope.launch(dispatchers.default) {
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken)
.onRight { tokenStatus ->
uiState.update {
YieldSupplyUM.Content(
title = resourceReference(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title,
yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken)
.onRight { tokenStatus ->
uiState.update {
YieldSupplyUM.Content(
title = resourceReference(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title,
),
subtitle = resourceReference(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle,
),
rewardsApy = combinedReference(
resourceReference(
R.string.yield_module_token_details_earn_notification_apy,
),
subtitle = resourceReference(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle,
),
rewardsApy = combinedReference(
resourceReference(
R.string.yield_module_token_details_earn_notification_apy,
),
stringReference(" ${tokenStatus.apy}%"),
),
onClick = ::onActiveClick,
showWarningIcon = showWarningIcon,
showInfoIcon = isShowInfoIconPrevState,
apy = tokenStatus.apy.toString(),
)
}
computeAndApplyShowInfoIcon(cryptoCurrencyStatus)
}.onLeft { t ->
Timber.e(t)
uiState.update {
YieldSupplyUM.Content(
title = resourceReference(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title,
),
subtitle = resourceReference(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle,
),
rewardsApy = TextReference.EMPTY,
onClick = ::onActiveClick,
showWarningIcon = showWarningIcon,
showInfoIcon = isShowInfoIconPrevState,
apy = "",
)
}
computeAndApplyShowInfoIcon(cryptoCurrencyStatus)
stringReference(" ${tokenStatus.apy}%"),
),
onClick = ::onActiveClick,
showWarningIcon = showWarningIcon,
showInfoIcon = isShowInfoIconPrevState,
apy = tokenStatus.apy.toString(),
)
}
}
computeAndApplyShowInfoIcon(cryptoCurrencyStatus)
}.onLeft { t ->
Timber.e(t)
uiState.update {
YieldSupplyUM.Content(
title = resourceReference(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title,
),
subtitle = resourceReference(
R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle,
),
rewardsApy = TextReference.EMPTY,
onClick = ::onActiveClick,
showWarningIcon = showWarningIcon,
showInfoIcon = isShowInfoIconPrevState,
apy = "",
)
}
computeAndApplyShowInfoIcon(cryptoCurrencyStatus)
}
}
private fun computeAndApplyShowInfoIcon(cryptoCurrencyStatus: CryptoCurrencyStatus) {

View file

@ -1,6 +1,7 @@
package com.tangem.features.yield.supply.impl.subcomponents.notifications
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -35,6 +36,7 @@ internal class YieldSupplyNotificationsComponent(
modifier = modifier
.fillMaxWidth()
.animateContentSize(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
state.forEachIndexed { index, item ->
Notification(

View file

@ -5,6 +5,7 @@ import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addHighFeeNotificationIfNoOther
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
@ -75,26 +76,41 @@ internal class YieldSupplyNotificationsModel @Inject constructor(
onReload = params.callback::onFeeReload,
)
if (data.shouldShowHighFeeNotification) {
add(NotificationUM.Info.YieldSupplyHighNetworkFee)
}
}
if (notifications.any { it is NotificationUM.Error.TokenExceedsBalance }) {
analyticsEventHandler.send(
YieldSupplyAnalytics.NoticeNotEnoughFee(
token = cryptoCurrencyStatus.currency.symbol,
blockchain = cryptoCurrencyStatus.currency.network.name,
),
addHighFeeNotificationIfNoOther(
shouldShowHighFeeNotification = data.shouldShowHighFeeNotification,
)
}
sendAnalytics(notifications, cryptoCurrencyStatus.currency)
uiState.update { notifications.toPersistentList() }
yieldSupplyNotificationsUpdateListener.callbackHasError(notifications.any())
// Business requirement that YieldSupplyHighNetworkFee is not an error and doesn't block the button
val hasError = notifications.any { it !is NotificationUM.Info.YieldSupplyHighNetworkFee }
yieldSupplyNotificationsUpdateListener.callbackHasError(hasError)
}.launchIn(modelScope)
}
private fun sendAnalytics(notifications: List<NotificationUM>, currency: CryptoCurrency) {
if (notifications.any { it is NotificationUM.Error.TokenExceedsBalance }) {
analyticsEventHandler.send(
YieldSupplyAnalytics.NoticeNotEnoughFee(
token = currency.symbol,
blockchain = currency.network.name,
),
)
}
if (notifications.any { it is NotificationUM.Info.YieldSupplyHighNetworkFee }) {
analyticsEventHandler.send(
YieldSupplyAnalytics.NoticeHighFee(
token = currency.symbol,
blockchain = currency.network.name,
),
)
}
}
private fun openTokenDetails(cryptoCurrency: CryptoCurrency) {
appRouter.push(
AppRoute.CurrencyDetails(

View file

@ -2,6 +2,7 @@ package com.tangem.sdk.api
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import arrow.core.Either
import com.tangem.Message
import com.tangem.common.CompletionResult
import com.tangem.common.KeyPair
@ -16,6 +17,7 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.WithdrawalSignatureResult
import com.tangem.domain.visa.model.TangemPayInitialCredentials
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaDataForApprove
@ -159,8 +161,8 @@ interface TangemSdkManager {
visaDataForApprove: VisaDataForApprove,
): CompletionResult<VisaSignedDataByCustomerWallet>
suspend fun tangemPayProduceInitialCredentials(cardId: String): CompletionResult<TangemPayInitialCredentials>
suspend fun tangemPayProduceInitialCredentials(cardId: String): Either<Throwable, TangemPayInitialCredentials>
suspend fun getWithdrawalSignature(cardId: String, hash: String): CompletionResult<String>
suspend fun getWithdrawalSignature(cardId: String, hash: String): Either<Throwable, WithdrawalSignatureResult>
// endregion
}