Updated on 2026-08-14
This commit is contained in:
commit
5732d139f1
72 changed files with 946 additions and 620 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -77,6 +77,9 @@ enum class NetworkTypeDTO {
|
|||
@Json(name = "canto")
|
||||
CANTO,
|
||||
|
||||
@Json(name = "cardano")
|
||||
CARDANO,
|
||||
|
||||
@Json(name = "chihuahua")
|
||||
CHIHUAHUA,
|
||||
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -80,9 +80,8 @@ internal interface TangemPayDataModule {
|
|||
deviceSecurity: DeviceSecurityInfoProvider,
|
||||
): TangemPayMainScreenCustomerInfoUseCase {
|
||||
return TangemPayMainScreenCustomerInfoUseCase(
|
||||
repository = repository,
|
||||
onboardingRepository = repository,
|
||||
customerOrderRepository = customerOrderRepository,
|
||||
tangemPayOnboardingRepository = tangemPayOnboardingRepository,
|
||||
eligibilityManager = eligibilityManager,
|
||||
deviceSecurity = deviceSecurity,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ enum class NetworkType {
|
|||
BAND_PROTOCOL,
|
||||
BITSONG,
|
||||
CANTO,
|
||||
CARDANO,
|
||||
CHIHUAHUA,
|
||||
COMDEX,
|
||||
COREUM,
|
||||
|
|
|
|||
|
|
@ -56,4 +56,8 @@ interface SettingsRepository {
|
|||
suspend fun setGooglePayAvailability(value: Boolean)
|
||||
|
||||
suspend fun isGooglePayAvailability(): Boolean
|
||||
|
||||
suspend fun isRootDetectedWarningShown(): Boolean
|
||||
|
||||
suspend fun setRootDetectedWarningShown(value: Boolean)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.domain.pay.model
|
||||
package com.tangem.domain.pay
|
||||
|
||||
sealed class WithdrawalSignatureResult {
|
||||
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -149,7 +149,6 @@ internal class AccessCodeModel @Inject constructor(
|
|||
onClick = {
|
||||
uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
accessCode = "",
|
||||
onAccessCodeChange = ::onAccessCodeChange,
|
||||
requestFocus = triggeredEvent(Unit, ::consumeRequestFocusEvent),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = "",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue