Updated on 2026-08-14
This commit is contained in:
commit
c1af0daa09
75 changed files with 788 additions and 526 deletions
|
|
@ -610,7 +610,7 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"id": "aurora-near",
|
||||
"id": "aurora-ethereum",
|
||||
"name": "Aurora Testnet",
|
||||
"symbol": "ETH",
|
||||
"networks": [
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.wallet.R
|
|||
|
||||
class CustomTabsManager {
|
||||
fun openUrl(url: String, context: Context) {
|
||||
if (url.isEmpty()) return
|
||||
val customTabsIntent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@ import androidx.annotation.StringRes
|
|||
import com.tangem.Message
|
||||
import com.tangem.TangemSdk
|
||||
import com.tangem.common.*
|
||||
import com.tangem.common.authentication.KeystoreManager
|
||||
import com.tangem.common.authentication.keystore.KeystoreManager
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.common.core.*
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.common.usersCode.UserCodeRepository
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.crypto.bip39.DefaultMnemonic
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
|
|
@ -25,7 +26,6 @@ import com.tangem.operations.derivation.DerivationTaskResponse
|
|||
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
|
||||
import com.tangem.operations.pins.SetUserCodeCommand
|
||||
import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.tap.derivationsFinder
|
||||
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
|
||||
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.tap.domain.userWalletList.di
|
|||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.tangem.common.authentication.AuthenticatedStorage
|
||||
import com.tangem.common.authentication.storage.AuthenticatedStorage
|
||||
import com.tangem.common.json.TangemSdkAdapter
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.tap.domain.userWalletList.di
|
|||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.tangem.common.authentication.AuthenticatedStorage
|
||||
import com.tangem.common.authentication.storage.AuthenticatedStorage
|
||||
import com.tangem.common.json.TangemSdkAdapter
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.domain.userWalletList.implementation
|
|||
import com.tangem.common.*
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListError
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
|
||||
|
|
@ -56,8 +57,8 @@ internal class BiometricUserWalletsListManager(
|
|||
override val walletsCount: Int
|
||||
get() = state.value.userWallets.size
|
||||
|
||||
override suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean): CompletionResult<UserWallet> {
|
||||
return unlockWithBiometryInternal()
|
||||
override suspend fun unlock(type: UnlockType): CompletionResult<UserWallet> {
|
||||
return unlockAndSetSelectedUserWallet(type)
|
||||
.mapFailure { error ->
|
||||
Timber.e(error, "Unable to unlock user wallets")
|
||||
if (error is UserWalletsListError) {
|
||||
|
|
@ -66,16 +67,8 @@ internal class BiometricUserWalletsListManager(
|
|||
UserWalletsListError.UnableToUnlockUserWallets(error)
|
||||
}
|
||||
}
|
||||
.map {
|
||||
val userWallets = state.value.userWallets
|
||||
|
||||
if (throwIfNotAllWalletsUnlocked && userWallets.any(UserWallet::isLocked)) {
|
||||
Timber.e("Some user wallets remain locked")
|
||||
throw UserWalletsListError.NotAllUserWalletsUnlocked
|
||||
}
|
||||
|
||||
val selectedUserWallet = selectedUserWalletSync
|
||||
if (selectedUserWallet == null) {
|
||||
.map { selectedUserWallet ->
|
||||
if (selectedUserWallet == null || selectedUserWallet.isLocked) {
|
||||
Timber.e("Unable to find selected user wallet")
|
||||
throw UserWalletsListError.NoUserWalletSelected
|
||||
} else {
|
||||
|
|
@ -186,99 +179,117 @@ internal class BiometricUserWalletsListManager(
|
|||
changeSelectedUserWallet: Boolean,
|
||||
canOverridePublicInfo: Boolean,
|
||||
): CompletionResult<Unit> {
|
||||
return saveEncryptionKeyIfNotNull(userWallet)
|
||||
.flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = it) }
|
||||
.flatMap { publicInformationRepository.save(userWallet, canOverridePublicInfo) }
|
||||
.map {
|
||||
if (changeSelectedUserWallet) {
|
||||
selectedUserWalletRepository.set(userWallet.walletId)
|
||||
}
|
||||
}
|
||||
.flatMap { loadModels() }
|
||||
.doOnSuccess {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
selectedUserWalletId = if (changeSelectedUserWallet) {
|
||||
userWallet.walletId
|
||||
} else {
|
||||
prevState.selectedUserWalletId
|
||||
},
|
||||
isLocked = prevState.userWallets.any { it.isLocked },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun unlockWithBiometryInternal(): CompletionResult<Unit> {
|
||||
return keysRepository.getAll()
|
||||
.map { keys ->
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
encryptionKeys = (keys + prevState.encryptionKeys).distinctBy { it.walletId },
|
||||
)
|
||||
}
|
||||
}
|
||||
.flatMap { loadModels() }
|
||||
.map {
|
||||
state.update { prevState ->
|
||||
val hasLockedUserWallets = prevState.userWallets.any { it.isLocked }
|
||||
prevState.copy(isLocked = hasLockedUserWallets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveEncryptionKeyIfNotNull(userWallet: UserWallet): CompletionResult<ByteArray?> {
|
||||
val encryptionKey = userWallet.scanResponse.card.encryptionKey
|
||||
?.let { UserWalletEncryptionKey(userWallet.walletId, it) }
|
||||
?: return CompletionResult.Success(Unit) // No encryption key, no need to save
|
||||
|
||||
return if (encryptionKey != null) {
|
||||
keysRepository.save(encryptionKey)
|
||||
.doOnSuccess {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
encryptionKeys = prevState.encryptionKeys
|
||||
.plus(encryptionKey)
|
||||
.distinctBy { it.walletId },
|
||||
)
|
||||
}
|
||||
return keysRepository.save(encryptionKey)
|
||||
.flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = encryptionKey.encryptionKey) }
|
||||
.flatMap { publicInformationRepository.save(userWallet, canOverridePublicInfo) }
|
||||
.flatMap {
|
||||
loadUserWallets(
|
||||
encryptionKeys = state.value.encryptionKeys
|
||||
.plus(encryptionKey)
|
||||
.distinctBy(UserWalletEncryptionKey::walletId),
|
||||
)
|
||||
}
|
||||
.doOnSuccess { loadedState ->
|
||||
if (changeSelectedUserWallet) {
|
||||
selectedUserWalletRepository.set(userWallet.walletId)
|
||||
|
||||
state.value = loadedState.copy(
|
||||
selectedUserWalletId = userWallet.walletId,
|
||||
)
|
||||
} else {
|
||||
state.value = loadedState
|
||||
}
|
||||
.map { encryptionKey.encryptionKey }
|
||||
} else {
|
||||
CompletionResult.Success(data = null)
|
||||
}
|
||||
}
|
||||
.map { /* Type erasing */ }
|
||||
}
|
||||
|
||||
private suspend fun loadModels(): CompletionResult<Unit> {
|
||||
return getSavedUserWallets()
|
||||
.map { userWallets ->
|
||||
if (userWallets.isNotEmpty()) {
|
||||
state.update { prevState ->
|
||||
val wallets = (userWallets + prevState.userWallets).distinctBy { it.walletId }
|
||||
private suspend fun unlockAndSetSelectedUserWallet(type: UnlockType): CompletionResult<UserWallet?> {
|
||||
return keysRepository.getAll()
|
||||
.flatMap { encryptionKeys ->
|
||||
loadUserWallets(
|
||||
encryptionKeys = state.value.encryptionKeys
|
||||
.plus(encryptionKeys)
|
||||
.distinctBy(UserWalletEncryptionKey::walletId),
|
||||
)
|
||||
}
|
||||
.map { loadedState ->
|
||||
when (type) {
|
||||
UnlockType.ALL -> {
|
||||
if (loadedState.isLocked) {
|
||||
Timber.e("Some user wallets remain locked")
|
||||
|
||||
prevState.copy(
|
||||
userWallets = wallets,
|
||||
selectedUserWalletId = findOrSetSelectedWalletId(prevState.selectedUserWalletId, wallets),
|
||||
state.value = loadedState
|
||||
|
||||
throw UserWalletsListError.NotAllUserWalletsUnlocked
|
||||
} else {
|
||||
val selectedWallet = findOrSetSelectedWallet(
|
||||
state.value.selectedUserWalletId,
|
||||
loadedState.userWallets,
|
||||
)
|
||||
|
||||
state.value = loadedState.copy(
|
||||
selectedUserWalletId = selectedWallet?.walletId,
|
||||
)
|
||||
|
||||
selectedWallet
|
||||
}
|
||||
}
|
||||
UnlockType.ANY -> {
|
||||
val selectedWallet = findOrSetSelectedWallet(
|
||||
state.value.selectedUserWalletId,
|
||||
loadedState.userWallets,
|
||||
)
|
||||
|
||||
state.value = loadedState.copy(
|
||||
selectedUserWalletId = selectedWallet?.walletId,
|
||||
)
|
||||
|
||||
selectedWallet
|
||||
}
|
||||
UnlockType.ALL_WITHOUT_SELECT -> {
|
||||
state.value = loadedState
|
||||
|
||||
findSelectedUserWallet()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getSavedUserWallets(): CompletionResult<List<UserWallet>> {
|
||||
private suspend fun loadUserWallets(encryptionKeys: List<UserWalletEncryptionKey>): CompletionResult<State> {
|
||||
return publicInformationRepository.getAll()
|
||||
.map { it.toUserWallets() }
|
||||
.flatMap { userWallets ->
|
||||
sensitiveInformationRepository.getAll(state.value.encryptionKeys)
|
||||
sensitiveInformationRepository.getAll(encryptionKeys)
|
||||
.map { walletIdToSensitiveInformation ->
|
||||
userWallets.updateWith(walletIdToSensitiveInformation)
|
||||
}
|
||||
}
|
||||
.map { userWallets ->
|
||||
val prevState = state.value
|
||||
|
||||
if (userWallets.isNotEmpty()) {
|
||||
val newUserWallets = (userWallets + prevState.userWallets)
|
||||
.distinctBy(UserWallet::walletId)
|
||||
|
||||
prevState.copy(
|
||||
userWallets = newUserWallets,
|
||||
encryptionKeys = encryptionKeys,
|
||||
isLocked = newUserWallets.any(UserWallet::isLocked),
|
||||
)
|
||||
} else {
|
||||
prevState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findOrSetSelectedWalletId(
|
||||
private fun findOrSetSelectedWallet(
|
||||
prevSelectedWalletId: UserWalletId?,
|
||||
userWallets: List<UserWallet>,
|
||||
): UserWalletId? {
|
||||
): UserWallet? {
|
||||
val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get()
|
||||
var possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId)
|
||||
|
||||
|
|
@ -290,7 +301,7 @@ internal class BiometricUserWalletsListManager(
|
|||
}
|
||||
}
|
||||
|
||||
return possibleSelectedUserWallet?.walletId
|
||||
return possibleSelectedUserWallet
|
||||
}
|
||||
|
||||
private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List<UserWalletId>) {
|
||||
|
|
|
|||
|
|
@ -100,10 +100,10 @@ internal class GeneralUserWalletsListManager(
|
|||
return implementation.value.get(userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean): CompletionResult<UserWallet> {
|
||||
override suspend fun unlock(type: UserWalletsListManager.Lockable.UnlockType): CompletionResult<UserWallet> {
|
||||
val implementation = implementation.value
|
||||
return if (implementation is UserWalletsListManager.Lockable) {
|
||||
implementation.unlock()
|
||||
implementation.unlock(type)
|
||||
} else {
|
||||
error("RuntimeUserWalletsListManager is not lockable")
|
||||
}
|
||||
|
|
@ -131,16 +131,25 @@ internal class GeneralUserWalletsListManager(
|
|||
Timber.d("Switch to ${manager::class.java.simpleName}")
|
||||
|
||||
implementation.value = manager
|
||||
|
||||
clearOldManager(manager)
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(applicationScope)
|
||||
}
|
||||
|
||||
/** Copy data from [old] manager and clean it */
|
||||
/** Copy data from [old] manager */
|
||||
private suspend fun UserWalletsListManager.copyFrom(old: UserWalletsListManager): UserWalletsListManager {
|
||||
old.selectedUserWalletSync?.let { this.save(it) }
|
||||
old.clear()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private suspend fun clearOldManager(current: UserWalletsListManager) {
|
||||
if (current == biometricUserWalletsListManager) {
|
||||
runtimeUserWalletsListManager.clear()
|
||||
} else {
|
||||
biometricUserWalletsListManager.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository
|
||||
|
||||
import com.tangem.common.authentication.KeystoreManager
|
||||
import com.tangem.common.authentication.keystore.KeystoreManager
|
||||
import com.tangem.utils.Provider
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
|
|
@ -8,15 +8,18 @@ internal class DelegatedKeystoreManager(
|
|||
private val keystoreManagerProvider: Provider<KeystoreManager>,
|
||||
) : KeystoreManager {
|
||||
|
||||
override suspend fun get(keyAlias: String): SecretKey? {
|
||||
return keystoreManagerProvider().get(keyAlias)
|
||||
override suspend fun get(masterKeyConfig: KeystoreManager.MasterKeyConfig, keyAlias: String): SecretKey? {
|
||||
return keystoreManagerProvider().get(masterKeyConfig, keyAlias)
|
||||
}
|
||||
|
||||
override suspend fun get(keyAliases: Collection<String>): Map<String, SecretKey> {
|
||||
return keystoreManagerProvider().get(keyAliases)
|
||||
override suspend fun get(
|
||||
masterKeyConfig: KeystoreManager.MasterKeyConfig,
|
||||
keyAliases: Set<String>,
|
||||
): Map<String, SecretKey> {
|
||||
return keystoreManagerProvider().get(masterKeyConfig, keyAliases)
|
||||
}
|
||||
|
||||
override suspend fun store(keyAlias: String, key: SecretKey) {
|
||||
return keystoreManagerProvider().store(keyAlias, key)
|
||||
override suspend fun store(masterKeyConfig: KeystoreManager.MasterKeyConfig, keyAlias: String, key: SecretKey) {
|
||||
keystoreManagerProvider().store(masterKeyConfig, keyAlias, key)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import com.squareup.moshi.JsonAdapter
|
|||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.common.*
|
||||
import com.tangem.common.authentication.AuthenticatedStorage
|
||||
import com.tangem.common.authentication.storage.AuthenticatedStorage
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListError
|
||||
|
|
@ -89,7 +89,6 @@ internal class BiometricUserWalletsKeysRepository(
|
|||
.doOnFailure { error ->
|
||||
when (error) {
|
||||
is TangemSdkError.KeystoreInvalidated -> {
|
||||
// If the biometric cryptography key was invalidated, then delete all encryption keys
|
||||
getUserWalletsIds().forEach { userWalletId ->
|
||||
deleteEncryptionKey(userWalletId)
|
||||
}
|
||||
|
|
@ -120,7 +119,7 @@ internal class BiometricUserWalletsKeysRepository(
|
|||
}
|
||||
|
||||
private fun deleteEncryptionKey(userWalletId: UserWalletId) {
|
||||
return authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
|
||||
authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name)
|
||||
}
|
||||
|
||||
private suspend fun getUserWalletsIds(): List<UserWalletId> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.models
|
||||
|
||||
internal enum class SupportBlockchainType {
|
||||
|
||||
SUPPORTED, UNSUPPORTED, UNABLE_TO_DETERMINE
|
||||
}
|
||||
|
|
@ -20,4 +20,6 @@ internal interface CustomTokenRouter {
|
|||
* @param blockchain blockchain to show alert
|
||||
*/
|
||||
fun openUnsupportedNetworkAlert(blockchain: Blockchain)
|
||||
|
||||
fun showGenericErrorAlertAndPopBack()
|
||||
}
|
||||
|
|
@ -27,4 +27,13 @@ internal class DefaultCustomTokenRouter : CustomTokenRouter {
|
|||
)
|
||||
store.dispatchDialogShow(alert)
|
||||
}
|
||||
|
||||
override fun showGenericErrorAlertAndPopBack() {
|
||||
val alert = AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.common_error,
|
||||
messageId = R.string.common_unknown_error,
|
||||
onOk = { store.dispatch(NavigationAction.PopBackTo()) },
|
||||
)
|
||||
store.dispatchDialogShow(alert)
|
||||
}
|
||||
}
|
||||
|
|
@ -696,14 +696,19 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
return derivationNetwork.derivationPath(derivationStyle)
|
||||
}
|
||||
|
||||
private fun isUnsupportedBlockchain(blockchain: Blockchain): Boolean {
|
||||
private fun getSupportBlockchainType(blockchain: Blockchain): SupportBlockchainType {
|
||||
return getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = { false },
|
||||
ifLeft = { SupportBlockchainType.UNABLE_TO_DETERMINE },
|
||||
ifRight = {
|
||||
!it.scanResponse.card.canHandleBlockchain(
|
||||
val canHandleBlockchain = it.scanResponse.card.canHandleBlockchain(
|
||||
blockchain = blockchain,
|
||||
cardTypesResolver = it.scanResponse.cardTypesResolver,
|
||||
)
|
||||
if (canHandleBlockchain) {
|
||||
SupportBlockchainType.SUPPORTED
|
||||
} else {
|
||||
SupportBlockchainType.UNSUPPORTED
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -823,9 +828,18 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
fun onAddCustomTokenClick() {
|
||||
if (!isNetworkSelected()) return
|
||||
val blockchain = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
if (isUnsupportedBlockchain(blockchain)) {
|
||||
featureRouter.openUnsupportedNetworkAlert(blockchain)
|
||||
return
|
||||
when (getSupportBlockchainType(blockchain)) {
|
||||
SupportBlockchainType.SUPPORTED -> {
|
||||
/* no-op */
|
||||
}
|
||||
SupportBlockchainType.UNSUPPORTED -> {
|
||||
featureRouter.openUnsupportedNetworkAlert(blockchain)
|
||||
return
|
||||
}
|
||||
SupportBlockchainType.UNABLE_TO_DETERMINE -> {
|
||||
featureRouter.showGenericErrorAlertAndPopBack()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val currency = when (getCustomTokenType()) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.compose.runtime.MutableState
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.navigation.email.EmailSender
|
||||
|
|
@ -145,7 +146,7 @@ internal class DetailsViewModel(
|
|||
}
|
||||
|
||||
private fun sendFeedback() {
|
||||
Analytics.send(Settings.ButtonSendFeedback())
|
||||
Analytics.send(Basic.ButtonSupport())
|
||||
if (feedbackManagerFeatureToggles.isLocalLogsEnabled) {
|
||||
mainScope.launch {
|
||||
val email = getSupportFeedbackEmailUseCase()
|
||||
|
|
|
|||
|
|
@ -11,20 +11,20 @@ internal sealed interface SaveWalletAction : Action {
|
|||
val backupCardsIds: Set<String>?,
|
||||
) : SaveWalletAction
|
||||
|
||||
object Save : SaveWalletAction {
|
||||
object Success : SaveWalletAction
|
||||
data object Save : SaveWalletAction {
|
||||
data object Success : SaveWalletAction
|
||||
data class Error(val error: TangemError) : SaveWalletAction
|
||||
}
|
||||
|
||||
object AllowToUseBiometrics : SaveWalletAction
|
||||
data object AllowToUseBiometrics : SaveWalletAction
|
||||
|
||||
object Dismiss : SaveWalletAction
|
||||
data object Dismiss : SaveWalletAction
|
||||
|
||||
object CloseError : SaveWalletAction
|
||||
object EnrollBiometrics : SaveWalletAction {
|
||||
object Enroll : SaveWalletAction
|
||||
object Cancel : SaveWalletAction
|
||||
data object CloseError : SaveWalletAction
|
||||
data object EnrollBiometrics : SaveWalletAction {
|
||||
data object Enroll : SaveWalletAction
|
||||
data object Cancel : SaveWalletAction
|
||||
}
|
||||
|
||||
object SaveWalletWasShown : SaveWalletAction
|
||||
data object SaveWalletWasShown : SaveWalletAction
|
||||
}
|
||||
|
|
@ -147,6 +147,11 @@ internal class SaveWalletMiddleware {
|
|||
}
|
||||
|
||||
private fun allowToUseBiometrics(state: SaveWalletState) {
|
||||
if (tangemSdkManager.needEnrollBiometrics) {
|
||||
store.dispatchOnMain(SaveWalletAction.EnrollBiometrics)
|
||||
return
|
||||
}
|
||||
|
||||
val scanResponse = state.backupInfo?.scanResponse
|
||||
?: store.state.globalState.scanResponse
|
||||
?: return
|
||||
|
|
|
|||
|
|
@ -50,6 +50,15 @@ internal class DefaultTokensListRouter : TokensListRouter {
|
|||
store.dispatchDialogShow(alert)
|
||||
}
|
||||
|
||||
override fun showGenericErrorAlertAndPopBack() {
|
||||
val alert = AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.common_error,
|
||||
messageId = R.string.common_unknown_error,
|
||||
onOk = { store.dispatch(NavigationAction.PopBackTo()) },
|
||||
)
|
||||
store.dispatchDialogShow(alert)
|
||||
}
|
||||
|
||||
override fun openNetworkTokensNotSupportAlert(networkName: String) {
|
||||
store.dispatchDialogShow(
|
||||
AppDialog.SimpleOkDialogRes(
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ internal interface TokensListRouter {
|
|||
*/
|
||||
fun openUnsupportedNetworkAlert(blockchain: Blockchain)
|
||||
|
||||
fun showGenericErrorAlertAndPopBack()
|
||||
|
||||
/**
|
||||
* Open alert with unsupported networks tokens error
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -96,5 +96,6 @@ internal sealed interface TokensListStateHolder {
|
|||
override val tokens: Flow<PagingData<TokenItemState>>,
|
||||
override val onTokensLoadStateChanged: (LoadState) -> Unit,
|
||||
val onSaveButtonClick: () -> Unit,
|
||||
val isSavingInProgress: Boolean,
|
||||
) : TokensListStateHolder
|
||||
}
|
||||
|
|
@ -29,7 +29,10 @@ import androidx.compose.ui.unit.TextUnit
|
|||
import androidx.compose.ui.unit.TextUnitType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.*
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.itemContentType
|
||||
import androidx.paging.compose.itemKey
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState
|
||||
|
|
@ -63,10 +66,11 @@ internal fun TokensListScreen(stateHolder: TokensListStateHolder, modifier: Modi
|
|||
val verticalPadding = TangemTheme.dimens.spacing32
|
||||
|
||||
SaveChangesButton(
|
||||
onClick = stateHolder.onSaveButtonClick,
|
||||
modifier = Modifier.onSizeChanged {
|
||||
with(density) { floatingButtonHeight = it.height.toDp() + verticalPadding }
|
||||
},
|
||||
showProgress = stateHolder.isSavingInProgress,
|
||||
onClick = stateHolder.onSaveButtonClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -176,13 +180,14 @@ private fun DifferentAddressesWarning() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveChangesButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
private fun SaveChangesButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
PrimaryButton(
|
||||
modifier = modifier
|
||||
.imePadding()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(id = R.string.common_save_changes),
|
||||
showProgress = showProgress,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -237,6 +242,7 @@ private class TokensListScreenProvider : CollectionPreviewParameterProvider<Toke
|
|||
),
|
||||
onSaveButtonClick = {},
|
||||
onTokensLoadStateChanged = {},
|
||||
isSavingInProgress = false,
|
||||
),
|
||||
TokensListStateHolder.ReadContent(
|
||||
toolbarState = TokensListToolbarState.Title.Read(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.features.tokens.impl.presentation.viewmodels
|
|||
import arrow.core.Either
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
|
|
@ -15,7 +16,7 @@ import com.tangem.domain.wallets.models.UserWallet
|
|||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -125,7 +126,7 @@ internal class TokensListMigration(
|
|||
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
|
||||
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
|
||||
store.dispatchDebugErrorNotification(message = "Nothing to save")
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
store.dispatchWithMain(NavigationAction.PopBackTo(screen = AppScreen.Wallet))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -134,9 +135,8 @@ internal class TokensListMigration(
|
|||
derivePublicKeysUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList)
|
||||
.onRight {
|
||||
addCryptoCurrenciesUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
}
|
||||
.onLeft { Timber.e("Failed to derive public keys: $it") }
|
||||
.onLeft { Timber.e(it, "Failed to derive public keys") }
|
||||
}
|
||||
|
||||
private suspend fun removeCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import androidx.lifecycle.viewModelScope
|
|||
import androidx.paging.*
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.core.ui.extensions.getGreyedOutIconRes
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
|
|
@ -24,8 +26,10 @@ import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
|||
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.TokenWithBlockchain
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.common.extensions.fullNameWithoutTestnet
|
||||
import com.tangem.tap.common.extensions.getNetworkName
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.SupportBlockchainType
|
||||
import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor
|
||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||
import com.tangem.tap.features.tokens.impl.domain.models.Token.Network
|
||||
|
|
@ -122,6 +126,7 @@ internal class TokensListViewModel @Inject constructor(
|
|||
isDifferentAddressesBlockVisible = isDifferentAddressesBlockVisible(),
|
||||
tokens = getInitialTokensList(),
|
||||
onTokensLoadStateChanged = actionsHandler::onTokensLoadStateChanged,
|
||||
isSavingInProgress = false,
|
||||
onSaveButtonClick = actionsHandler::onSaveButtonClick,
|
||||
)
|
||||
} else {
|
||||
|
|
@ -308,13 +313,20 @@ internal class TokensListViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onSaveButtonClick() {
|
||||
val state = uiState as? TokensListStateHolder.ManageContent ?: return
|
||||
|
||||
analyticsSender.sendWhenSaveButtonClicked()
|
||||
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
uiState = state.copy(isSavingInProgress = true)
|
||||
|
||||
tokensListMigration.onSaveButtonClick(
|
||||
changedTokensList = changedTokensList,
|
||||
changedBlockchainList = changedBlockchainList,
|
||||
)
|
||||
|
||||
uiState = state.copy(isSavingInProgress = false)
|
||||
store.dispatchWithMain(NavigationAction.PopBackTo(screen = AppScreen.Wallet))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -369,12 +381,18 @@ internal class TokensListViewModel @Inject constructor(
|
|||
toggledNetwork.changeToggleState()
|
||||
}
|
||||
} else {
|
||||
if (isUnsupportedBlockchain(blockchain)) {
|
||||
router.openUnsupportedNetworkAlert(blockchain)
|
||||
} else {
|
||||
analyticsSender.sendWhenBlockchainAdded(blockchain)
|
||||
changedBlockchainList.add(blockchain)
|
||||
toggledNetwork.changeToggleState()
|
||||
when (getSupportBlockchainType(blockchain)) {
|
||||
SupportBlockchainType.SUPPORTED -> {
|
||||
analyticsSender.sendWhenBlockchainAdded(blockchain)
|
||||
changedBlockchainList.add(blockchain)
|
||||
toggledNetwork.changeToggleState()
|
||||
}
|
||||
SupportBlockchainType.UNSUPPORTED -> {
|
||||
router.openUnsupportedNetworkAlert(blockchain)
|
||||
}
|
||||
SupportBlockchainType.UNABLE_TO_DETERMINE -> {
|
||||
router.showGenericErrorAlertAndPopBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -458,14 +476,19 @@ internal class TokensListViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun isUnsupportedBlockchain(blockchain: Blockchain): Boolean {
|
||||
private fun getSupportBlockchainType(blockchain: Blockchain): SupportBlockchainType {
|
||||
return getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = { false },
|
||||
ifLeft = { SupportBlockchainType.UNABLE_TO_DETERMINE },
|
||||
ifRight = {
|
||||
!it.scanResponse.card.canHandleBlockchain(
|
||||
val canHandleBlockchain = it.scanResponse.card.canHandleBlockchain(
|
||||
blockchain = blockchain,
|
||||
cardTypesResolver = it.scanResponse.cardTypesResolver,
|
||||
)
|
||||
if (canHandleBlockchain) {
|
||||
SupportBlockchainType.SUPPORTED
|
||||
} else {
|
||||
SupportBlockchainType.UNSUPPORTED
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.core.navigation.NavigationAction
|
|||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.userwallets.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
|
||||
import com.tangem.domain.wallets.legacy.unlockIfLockable
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
|
||||
|
|
@ -86,7 +87,7 @@ internal class WelcomeMiddleware {
|
|||
""".trimIndent(),
|
||||
)
|
||||
|
||||
userWalletsListManager.unlockIfLockable()
|
||||
userWalletsListManager.unlockIfLockable(type = UnlockType.ANY)
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Unable to unlock user wallets with biometrics")
|
||||
store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Error(error))
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ sealed class AnalyticsParam {
|
|||
companion object
|
||||
}
|
||||
|
||||
sealed class TokenBalanceState(val value: String) {
|
||||
object Empty : TokenBalanceState("Empty")
|
||||
object Full : TokenBalanceState("Full")
|
||||
}
|
||||
|
||||
sealed class RateApp(val value: String) {
|
||||
object Liked : RateApp("Liked")
|
||||
object Disliked : RateApp("Disliked")
|
||||
|
|
@ -124,6 +129,7 @@ sealed class AnalyticsParam {
|
|||
const val TOKEN = "Token"
|
||||
const val SOURCE = "Source"
|
||||
const val BALANCE = "Balance"
|
||||
const val STATE = "State"
|
||||
const val BATCH = "Batch"
|
||||
const val TYPE = "Type"
|
||||
const val FEE_TYPE = "Fee Type"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import timber.log.Timber
|
||||
|
||||
@Deprecated("Use shared preferences data store instead")
|
||||
|
|
@ -16,6 +18,7 @@ internal class FileDataStore<Value : Any>(
|
|||
private val adapter: JsonAdapter<Value>,
|
||||
) : StringKeyDataStore<Value> {
|
||||
|
||||
private val mutex = Mutex()
|
||||
private val writeTrigger = Trigger()
|
||||
override suspend fun isEmpty(): Boolean {
|
||||
val e = NotImplementedError("`isEmpty()` function not implemented for `FileDataStore`")
|
||||
|
|
@ -28,7 +31,11 @@ internal class FileDataStore<Value : Any>(
|
|||
|
||||
override fun get(key: String): Flow<Value> {
|
||||
return writeTrigger
|
||||
.map { getInternal(key) }
|
||||
.map {
|
||||
mutex.withLock {
|
||||
getInternal(key)
|
||||
}
|
||||
}
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
|
@ -53,10 +60,12 @@ internal class FileDataStore<Value : Any>(
|
|||
|
||||
override suspend fun store(key: String, value: Value) {
|
||||
try {
|
||||
val json = adapter.toJson(value)
|
||||
mutex.withLock {
|
||||
val json = adapter.toJson(value)
|
||||
|
||||
fileReader.rewriteFile(json, key)
|
||||
writeTrigger.trigger()
|
||||
fileReader.rewriteFile(json, key)
|
||||
writeTrigger.trigger()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to write file: $key")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
{
|
||||
"name": "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED",
|
||||
"version": "5.7.0"
|
||||
"version": "5.8.0"
|
||||
},
|
||||
{
|
||||
"name": "LOCAL_USER_LOGS_ENABLED",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
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.TextOverflow
|
||||
|
|
@ -86,7 +87,8 @@ private fun Button(
|
|||
Row(
|
||||
modifier = modifier
|
||||
.heightIn(min = TangemTheme.dimens.size36)
|
||||
.background(color = backgroundColor, shape = shape)
|
||||
.clip(shape)
|
||||
.background(color = backgroundColor)
|
||||
.clickable(enabled = config.enabled, onClick = config.onClick)
|
||||
.padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing24)
|
||||
.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
|
|
|
|||
|
|
@ -3,15 +3,21 @@ package com.tangem.data.common.cache
|
|||
import com.tangem.datasource.local.cache.CacheKeysStore
|
||||
import com.tangem.datasource.local.cache.model.CacheKey
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.joda.time.Duration
|
||||
import org.joda.time.LocalDateTime
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
internal class DefaultCacheRegistry(
|
||||
private val cacheKeysStore: CacheKeysStore,
|
||||
) : CacheRegistry {
|
||||
|
||||
private val mutex = Mutex()
|
||||
private val mutexes = ConcurrentHashMap<String, Mutex>()
|
||||
|
||||
override suspend fun isExpired(key: String): Boolean {
|
||||
val cacheKey = cacheKeysStore.getSyncOrNull(key) ?: return true
|
||||
|
||||
|
|
@ -41,27 +47,36 @@ internal class DefaultCacheRegistry(
|
|||
expireIn: Duration,
|
||||
block: suspend () -> Unit,
|
||||
) {
|
||||
val isExpired = isExpired(key) || skipCache
|
||||
if (!isExpired) return
|
||||
// use a separate mutexForKey for each key to avoid multiple calls block() to the same key
|
||||
// also used mutex to safe create mutexForKey, otherwise it can lead to multiple calls for the same key
|
||||
val mutexForKey = mutex.withLock {
|
||||
mutexes.getOrPut(key) { Mutex() }
|
||||
}
|
||||
mutexForKey.withLock {
|
||||
val isExpired = isExpired(key) || skipCache
|
||||
if (!isExpired) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Timber.d("Invoke the action associated with the cache key: $key")
|
||||
try {
|
||||
Timber.d("Invoke the action associated with the cache key: $key")
|
||||
|
||||
cacheKeysStore.store(
|
||||
key = CacheKey(
|
||||
id = key,
|
||||
updatedAt = LocalDateTime.now(),
|
||||
expiresIn = expireIn,
|
||||
),
|
||||
)
|
||||
cacheKeysStore.store(
|
||||
key = CacheKey(
|
||||
id = key,
|
||||
updatedAt = LocalDateTime.now(),
|
||||
expiresIn = expireIn,
|
||||
),
|
||||
)
|
||||
|
||||
block()
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "The action related to the cache key has failed: $key")
|
||||
block()
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "The action related to the cache key has failed: $key")
|
||||
|
||||
invalidate(key)
|
||||
invalidate(key)
|
||||
|
||||
throw e
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -56,8 +56,9 @@ internal class DefaultNetworksRepository(
|
|||
.cancellable()
|
||||
|
||||
override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set<Network>) {
|
||||
val currencies = getCurrencies(userWalletId, networks)
|
||||
withContext(dispatchers.io) {
|
||||
fetchNetworksPendingTransactions(userWalletId, networks)
|
||||
fetchNetworksPendingTransactions(userWalletId, networks, currencies)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,23 +81,28 @@ internal class DefaultNetworksRepository(
|
|||
networks: Set<Network>,
|
||||
refresh: Boolean,
|
||||
) {
|
||||
val currencies = getCurrencies(userWalletId, networks)
|
||||
coroutineScope {
|
||||
networks
|
||||
.map { network ->
|
||||
async {
|
||||
fetchNetworkStatusIfCacheExpired(userWalletId, network, refresh)
|
||||
fetchNetworkStatusIfCacheExpired(userWalletId, network, currencies, refresh)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworksPendingTransactions(userWalletId: UserWalletId, networks: Set<Network>) {
|
||||
private suspend fun fetchNetworksPendingTransactions(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network>,
|
||||
currencies: Sequence<CryptoCurrency>,
|
||||
) {
|
||||
coroutineScope {
|
||||
networks
|
||||
.map { network ->
|
||||
async {
|
||||
fetchNetworkPendingTransactions(userWalletId, network)
|
||||
fetchNetworkPendingTransactions(userWalletId, network, currencies)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
|
|
@ -106,22 +112,28 @@ internal class DefaultNetworksRepository(
|
|||
private suspend fun fetchNetworkStatusIfCacheExpired(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
currencies: Sequence<CryptoCurrency>,
|
||||
refresh: Boolean,
|
||||
) {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getNetworksStatusesCacheKey(userWalletId, network),
|
||||
skipCache = refresh,
|
||||
block = { fetchNetworkStatus(userWalletId, network) },
|
||||
block = { fetchNetworkStatus(userWalletId, network, currencies) },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, network: Network) {
|
||||
val currencies = getCurrencies(userWalletId, network)
|
||||
|
||||
private suspend fun fetchNetworkStatus(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
currencies: Sequence<CryptoCurrency>,
|
||||
) {
|
||||
val result = walletManagersFacade.update(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
extraTokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
|
||||
extraTokens = currencies
|
||||
.filterIsInstance<CryptoCurrency.Token>()
|
||||
.filter { it.network == network }
|
||||
.toSet(),
|
||||
)
|
||||
|
||||
withContext(NonCancellable) {
|
||||
|
|
@ -137,9 +149,11 @@ internal class DefaultNetworksRepository(
|
|||
networksStatusesStore.store(userWalletId, networkStatus)
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, network: Network) {
|
||||
val currencies = getCurrencies(userWalletId, network)
|
||||
|
||||
private suspend fun fetchNetworkPendingTransactions(
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
currencies: Sequence<CryptoCurrency>,
|
||||
) {
|
||||
val result = walletManagersFacade.updatePendingTransactions(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
|
|
@ -158,7 +172,7 @@ internal class DefaultNetworksRepository(
|
|||
networksStatusesStore.store(userWalletId, networkStatus)
|
||||
}
|
||||
|
||||
private suspend fun getCurrencies(userWalletId: UserWalletId, network: Network): Sequence<CryptoCurrency> {
|
||||
private suspend fun getCurrencies(userWalletId: UserWalletId, networks: Set<Network>): Sequence<CryptoCurrency> {
|
||||
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
|
||||
"Unable to find user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
|
|
@ -180,7 +194,7 @@ internal class DefaultNetworksRepository(
|
|||
}
|
||||
}
|
||||
|
||||
return currencies.filter { it.network == network }
|
||||
return currencies.filter { networks.contains(it.network) }
|
||||
}
|
||||
|
||||
private suspend fun invalidateCacheKeyIfNeeded(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import com.tangem.domain.tokens.repository.QuotesRepository
|
|||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultQuotesRepository(
|
||||
|
|
@ -32,6 +34,7 @@ internal class DefaultQuotesRepository(
|
|||
|
||||
@Volatile
|
||||
private var quotesFetchedForAppCurrency: String? = null
|
||||
private val mutex = Mutex()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<Quote>> {
|
||||
|
|
@ -42,7 +45,6 @@ internal class DefaultQuotesRepository(
|
|||
.filterNotNull()
|
||||
.flatMapLatest { appCurrency ->
|
||||
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false)
|
||||
|
||||
quotesStore.get(currenciesIds).map(quotesConverter::convertSet)
|
||||
}
|
||||
.cancellable()
|
||||
|
|
@ -79,15 +81,19 @@ internal class DefaultQuotesRepository(
|
|||
appCurrencyId: String,
|
||||
refresh: Boolean,
|
||||
) {
|
||||
val expiredCurrenciesIds = filterExpiredCurrenciesIds(
|
||||
currenciesIds = currenciesIds,
|
||||
refresh = refresh || quotesFetchedForAppCurrency != appCurrencyId,
|
||||
)
|
||||
if (expiredCurrenciesIds.isEmpty()) return
|
||||
// TODO("[REDACTED_JIRA]") need refactor working with quotesFetchedForAppCurrency,
|
||||
// it changes after filterExpiredCurrenciesIds
|
||||
// calls with different coroutines and lead to fetchQuotes
|
||||
mutex.withLock {
|
||||
val expiredCurrenciesIds = filterExpiredCurrenciesIds(
|
||||
currenciesIds = currenciesIds,
|
||||
refresh = refresh || quotesFetchedForAppCurrency != appCurrencyId,
|
||||
)
|
||||
if (expiredCurrenciesIds.isEmpty()) return
|
||||
|
||||
quotesFetchedForAppCurrency = appCurrencyId
|
||||
|
||||
fetchQuotes(expiredCurrenciesIds, appCurrencyId)
|
||||
quotesFetchedForAppCurrency = appCurrencyId
|
||||
fetchQuotes(expiredCurrenciesIds, appCurrencyId)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchQuotes(rawCurrenciesIds: Set<String>, appCurrencyId: String) {
|
||||
|
|
@ -118,7 +124,6 @@ internal class DefaultQuotesRepository(
|
|||
): Set<String> {
|
||||
return currenciesIds.fold(hashSetOf()) { acc, currencyId ->
|
||||
val rawCurrencyId = currencyId.rawCurrencyId
|
||||
|
||||
if (rawCurrencyId != null && rawCurrencyId !in acc) {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getQuoteCacheKey(rawCurrencyId),
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ internal class QuotesUnsupportedCurrenciesIdAdapter {
|
|||
private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = mapOf(
|
||||
"optimistic-ethereum" to "ethereum",
|
||||
"arbitrum-one" to "ethereum",
|
||||
"aurora-ethereum" to "ethereum",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -256,7 +256,7 @@ fun Blockchain.toCoinId(): String {
|
|||
Blockchain.Unknown -> "unknown"
|
||||
Blockchain.Hedera -> "hedera-hashgraph"
|
||||
Blockchain.HederaTestnet -> "hedera-hashgraph/test"
|
||||
Blockchain.Aurora, Blockchain.AuroraTestnet -> "aurora-near"
|
||||
Blockchain.Aurora, Blockchain.AuroraTestnet -> "aurora-ethereum"
|
||||
Blockchain.Areon, Blockchain.AreonTestnet -> "areon-network"
|
||||
Blockchain.PulseChain, Blockchain.PulseChainTestnet -> "pulsechain"
|
||||
Blockchain.ZkSyncEra -> "zksync" // FIXME
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.domain.txhistory.models
|
||||
|
||||
sealed class TxStatusError {
|
||||
|
||||
data object EmptyUrlError : TxStatusError()
|
||||
data class DataError(val cause: Throwable) : TxStatusError()
|
||||
}
|
||||
|
|
@ -1,12 +1,25 @@
|
|||
package com.tangem.domain.txhistory.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.txhistory.models.TxStatusError
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepository
|
||||
|
||||
class GetExplorerTransactionUrlUseCase(
|
||||
private val repository: TxHistoryRepository,
|
||||
) {
|
||||
operator fun invoke(txHash: String, networkId: Network.ID): String {
|
||||
return repository.getTxExploreUrl(txHash, networkId)
|
||||
operator fun invoke(txHash: String, networkId: Network.ID): Either<TxStatusError, String> {
|
||||
return either {
|
||||
catch(
|
||||
block = {
|
||||
repository.getTxExploreUrl(txHash, networkId).ifEmpty {
|
||||
raise(TxStatusError.EmptyUrlError)
|
||||
}
|
||||
},
|
||||
catch = { raise(TxStatusError.DataError(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -105,16 +105,42 @@ interface UserWalletsListManager {
|
|||
/**
|
||||
* Receive saved [UserWallet]s, populate [userWallets] flow with it and set [isLocked] as false.
|
||||
*
|
||||
* @param throwIfNotAllWalletsUnlocked Indicates that the function must throw
|
||||
* [UserWalletsListError.NotAllUserWalletsUnlocked] if not all user wallets are unlocked.
|
||||
* @param type Defines the behavior of the operation.
|
||||
*
|
||||
* @return [CompletionResult] of operation, with selected [UserWallet]
|
||||
* or null if there is no selected [UserWallet]
|
||||
*/
|
||||
suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean = false): CompletionResult<UserWallet>
|
||||
suspend fun unlock(type: UnlockType): CompletionResult<UserWallet>
|
||||
|
||||
/** Remove [UserWallet]s from [userWallets] and set [isLocked] as true */
|
||||
fun lock()
|
||||
|
||||
/**
|
||||
* Defines the behavior of the [unlock] operation.
|
||||
* */
|
||||
enum class UnlockType {
|
||||
/**
|
||||
* Ensures that all stored [UserWallet]s are unlocked,
|
||||
* or throws [UserWalletsListError.NotAllUserWalletsUnlocked].
|
||||
*
|
||||
* In this type [selectedUserWallet] is either a previously selected [UserWallet] or the first stored
|
||||
* [UserWallet].
|
||||
* */
|
||||
ALL,
|
||||
|
||||
/**
|
||||
* Ensures that at least one stored [UserWallet] is unlocked,
|
||||
* or throws [UserWalletsListError.NoUserWalletSelected].
|
||||
*
|
||||
* In this type [selectedUserWallet] is the first stored and unlocked [UserWallet].
|
||||
* */
|
||||
ANY,
|
||||
|
||||
/**
|
||||
* Same as [ALL] type, but this type can not change [selectedUserWallet] while unlocking.
|
||||
* */
|
||||
ALL_WITHOUT_SELECT,
|
||||
}
|
||||
}
|
||||
|
||||
// For provider
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.wallets.legacy
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
|
@ -43,8 +44,8 @@ val UserWalletsListManager.isLockedSync: Boolean
|
|||
*
|
||||
* @see UserWalletsListManager.Lockable.unlock
|
||||
* */
|
||||
suspend fun UserWalletsListManager.unlockIfLockable(): CompletionResult<UserWallet> {
|
||||
return asLockable()?.unlock() ?: CompletionResult.Failure(UserWalletsListError.UnableToUnlockUserWallets())
|
||||
suspend fun UserWalletsListManager.unlockIfLockable(type: UnlockType = UnlockType.ANY): CompletionResult<UserWallet> {
|
||||
return asLockable()?.unlock(type) ?: CompletionResult.Failure(UserWalletsListError.UnableToUnlockUserWallets())
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.firstOrNull
|
|||
|
||||
class GetUserWalletUseCase(private val walletsStateHolder: WalletsStateHolder) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId): Either<Any, UserWallet> = either {
|
||||
suspend operator fun invoke(userWalletId: UserWalletId): Either<GetUserWalletError, UserWallet> = either {
|
||||
val userWalletsListManager = ensureUserWalletListManagerNotNull(
|
||||
walletsStateHolder = walletsStateHolder,
|
||||
raise = GetUserWalletError::DataError,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import arrow.core.raise.either
|
|||
import arrow.core.raise.ensureNotNull
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListError
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
|
||||
import com.tangem.domain.wallets.legacy.WalletsStateHolder
|
||||
import com.tangem.domain.wallets.legacy.asLockable
|
||||
import com.tangem.domain.wallets.models.UnlockWalletsError
|
||||
|
|
@ -18,27 +19,26 @@ import com.tangem.domain.wallets.models.UnlockWalletsError
|
|||
*/
|
||||
class UnlockWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) {
|
||||
|
||||
suspend operator fun invoke(throwIfNotAllWalletsUnlocked: Boolean = false): Either<UnlockWalletsError, Unit> =
|
||||
either {
|
||||
val userWalletsListManager = ensureNotNull(
|
||||
value = walletsStateHolder.userWalletsListManager?.asLockable(),
|
||||
raise = {
|
||||
UnlockWalletsError.DataError(
|
||||
cause = IllegalStateException("The lockable user wallets list manager could not be found"),
|
||||
)
|
||||
},
|
||||
)
|
||||
suspend operator fun invoke(type: UnlockType = UnlockType.ANY): Either<UnlockWalletsError, Unit> = either {
|
||||
val userWalletsListManager = ensureNotNull(
|
||||
value = walletsStateHolder.userWalletsListManager?.asLockable(),
|
||||
raise = {
|
||||
UnlockWalletsError.DataError(
|
||||
cause = IllegalStateException("The lockable user wallets list manager could not be found"),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
userWalletsListManager.unlock(throwIfNotAllWalletsUnlocked)
|
||||
.doOnFailure { error ->
|
||||
val e = when (error) {
|
||||
is UserWalletsListError.NoUserWalletSelected -> UnlockWalletsError.NoUserWalletSelected
|
||||
is UserWalletsListError.NotAllUserWalletsUnlocked ->
|
||||
UnlockWalletsError.NotAllUserWalletsUnlocked
|
||||
else -> UnlockWalletsError.UnableToUnlockWallets
|
||||
}
|
||||
|
||||
raise(e)
|
||||
userWalletsListManager.unlock(type)
|
||||
.doOnFailure { error ->
|
||||
val e = when (error) {
|
||||
is UserWalletsListError.NoUserWalletSelected -> UnlockWalletsError.NoUserWalletSelected
|
||||
is UserWalletsListError.NotAllUserWalletsUnlocked ->
|
||||
UnlockWalletsError.NotAllUserWalletsUnlocked
|
||||
else -> UnlockWalletsError.UnableToUnlockWallets
|
||||
}
|
||||
}
|
||||
|
||||
raise(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,10 +20,10 @@ import com.google.mlkit.vision.common.InputImage
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningContent
|
||||
import com.tangem.feature.qrscanning.viewmodel.QrScanningViewModel
|
||||
import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer
|
||||
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningContent
|
||||
import com.tangem.feature.qrscanning.viewmodel.QrScanningViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
|
@ -47,6 +47,11 @@ internal class QrScanningFragment : ComposeFragment() {
|
|||
private val viewModel by viewModels<QrScanningViewModel>()
|
||||
|
||||
private var cameraExecutor: ExecutorService by Delegates.notNull()
|
||||
// Camera requires its own analyzer instance due to flow of frames needed to be analyzed.
|
||||
// Each new frame can cancel previous analysis e.i. image from the gallery can be skipped.
|
||||
private val cameraAnalyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) {
|
||||
MLKitBarcodeAnalyzer(viewModel::onQrScanned)
|
||||
}
|
||||
private val analyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) {
|
||||
MLKitBarcodeAnalyzer(viewModel::onQrScanned)
|
||||
}
|
||||
|
|
@ -69,6 +74,11 @@ internal class QrScanningFragment : ComposeFragment() {
|
|||
requestCameraPermission()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
checkPermissionGranted()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
cameraPermissionLauncher.unregister()
|
||||
|
|
@ -80,11 +90,14 @@ internal class QrScanningFragment : ComposeFragment() {
|
|||
StatusBarTransparencyDisposable()
|
||||
QrScanningContent(
|
||||
executor = { cameraExecutor },
|
||||
analyzer = { analyzer },
|
||||
analyzer = { cameraAnalyzer },
|
||||
uiState = viewModel.uiState.collectAsStateWithLifecycle().value,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for requesting permission if there isn't one.
|
||||
*/
|
||||
private fun requestCameraPermission() {
|
||||
if (
|
||||
ContextCompat.checkSelfPermission(
|
||||
|
|
@ -96,6 +109,20 @@ internal class QrScanningFragment : ComposeFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for checking if permission was granted after user opened Settings screen.
|
||||
* If permission was granted dismiss bottom sheet.
|
||||
*/
|
||||
private fun checkPermissionGranted() {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.CAMERA,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
viewModel.onDismissBottomSheetState()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun create() = QrScanningFragment()
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ private fun CameraDeniedBottomSheet(content: CameraDeniedBottomSheetConfig) {
|
|||
val intent: Intent = Intent(
|
||||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.fromParts("package", context.packageName, null),
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ import com.tangem.domain.qrscanning.models.SourceType
|
|||
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningState
|
||||
import com.tangem.feature.qrscanning.presentation.QrScanningStateController
|
||||
import com.tangem.feature.qrscanning.presentation.transformers.ShowCameraDeniedBottomSheetTransformer
|
||||
import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomSheetTransformer
|
||||
import com.tangem.feature.qrscanning.presentation.transformers.InitializeQrScanningStateTransformer
|
||||
import com.tangem.feature.qrscanning.presentation.transformers.ShowCameraDeniedBottomSheetTransformer
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
|
@ -43,4 +44,8 @@ internal class QrScanningViewModel @Inject constructor(
|
|||
fun onCameraDeniedState() {
|
||||
stateHolder.update(ShowCameraDeniedBottomSheetTransformer(clickIntents))
|
||||
}
|
||||
|
||||
fun onDismissBottomSheetState() {
|
||||
stateHolder.update(DismissBottomSheetTransformer())
|
||||
}
|
||||
}
|
||||
|
|
@ -248,7 +248,7 @@ internal class SendStateFactory(
|
|||
val txUrl = getExplorerTransactionUrlUseCase(
|
||||
txHash = txData.hash.orEmpty(),
|
||||
networkId = cryptoCurrency.network.id,
|
||||
)
|
||||
).getOrElse { "" }
|
||||
return state.copy(
|
||||
sendState = state.sendState.copy(
|
||||
transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ internal class ExchangeStatusConverter : Converter<ExchangeStatusResponse, Excha
|
|||
},
|
||||
txId = value.externalTxId,
|
||||
txExternalUrl = value.externalTxUrl,
|
||||
txExternalId = value.externalTxId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1484,7 +1484,16 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
fromTokenStatus.currency.network.derivationPath.value,
|
||||
)
|
||||
nativeTokenBalance?.let { balance ->
|
||||
if (balance.value.minus(spendAmount.value) > fee.multiply(percentsToFeeIncrease)) {
|
||||
val balanceToCheck = when (fromTokenStatus.currency) {
|
||||
is CryptoCurrency.Token -> {
|
||||
balance.value
|
||||
}
|
||||
is CryptoCurrency.Coin -> {
|
||||
// need to check balance minus amount only if amount to swap in native token
|
||||
balance.value.minus(spendAmount.value)
|
||||
}
|
||||
}
|
||||
if (balanceToCheck > fee.multiply(percentsToFeeIncrease)) {
|
||||
SwapFeeState.Enough
|
||||
} else {
|
||||
val nativeToken = getNativeToken(fromTokenStatus.currency.network.backendId)
|
||||
|
|
@ -1514,9 +1523,9 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
} else {
|
||||
val token = currenciesRepository
|
||||
.getMultiCurrencyWalletCurrenciesSync(userWalletId)
|
||||
.filterIsInstance<CryptoCurrency.Token>()
|
||||
.find {
|
||||
it is CryptoCurrency.Token &&
|
||||
it.contractAddress.equals(feePaidCurrency.contractAddress, ignoreCase = true) &&
|
||||
it.contractAddress.equals(feePaidCurrency.contractAddress, ignoreCase = true) &&
|
||||
it.network.derivationPath == fromTokenStatus.currency.network.derivationPath
|
||||
}
|
||||
SwapFeeState.NotEnough(
|
||||
|
|
|
|||
|
|
@ -793,6 +793,7 @@ internal class StateBuilder(
|
|||
return uiState.copy(
|
||||
sendCardData = uiState.sendCardData.copy(
|
||||
balance = cryptoCurrencyStatus.getFormattedAmount(isNeedSymbol = false),
|
||||
token = cryptoCurrencyStatus,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -806,6 +807,7 @@ internal class StateBuilder(
|
|||
return uiState.copy(
|
||||
receiveCardData = uiState.receiveCardData.copy(
|
||||
balance = cryptoCurrencyStatus.getFormattedAmount(isNeedSymbol = false),
|
||||
token = cryptoCurrencyStatus,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -746,6 +746,8 @@ internal class SwapViewModel @Inject constructor(
|
|||
dataState = dataState.copy(toCryptoCurrency = it)
|
||||
stateBuilder.updateReceiveCurrencyBalance(uiState, it)
|
||||
}
|
||||
|
||||
startLoadingQuotesFromLastState(isSilent = true)
|
||||
}
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(viewModelScope)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.lifecycle.*
|
||||
import androidx.paging.cachedIn
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
|
|
@ -601,20 +600,13 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onTransactionClick(txHash: String) {
|
||||
// TODO: Fix tx urls [REDACTED_TASK_KEY]
|
||||
when (Blockchain.fromId(cryptoCurrency.network.id.value)) {
|
||||
Blockchain.TON, Blockchain.TONTestnet,
|
||||
Blockchain.Decimal, Blockchain.DecimalTestnet,
|
||||
-> return
|
||||
else -> {
|
||||
router.openUrl(
|
||||
url = getExplorerTransactionUrlUseCase(
|
||||
txHash = txHash,
|
||||
networkId = cryptoCurrency.network.id,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
getExplorerTransactionUrlUseCase(
|
||||
txHash = txHash,
|
||||
networkId = cryptoCurrency.network.id,
|
||||
).fold(
|
||||
ifLeft = { Timber.e(it.toString()) },
|
||||
ifRight = { router.openUrl(url = it) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRefreshSwipe() {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,14 @@ sealed class WalletScreenAnalyticsEvent {
|
|||
AnalyticsParam.BALANCE to balance.value,
|
||||
),
|
||||
)
|
||||
|
||||
class TokenBalance(balance: AnalyticsParam.TokenBalanceState, token: String) : Basic(
|
||||
event = "Token Balance",
|
||||
params = mapOf(
|
||||
AnalyticsParam.STATE to balance.value,
|
||||
AnalyticsParam.TOKEN to token,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class MainScreen(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.analytics.utils
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase
|
||||
import com.tangem.domain.analytics.model.WalletBalanceState
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.NetworkGroup
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
|
|
@ -14,6 +17,8 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnaly
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -24,6 +29,9 @@ internal class TokenListAnalyticsSender @Inject constructor(
|
|||
private val screenLifecycleProvider: ScreenLifecycleProvider,
|
||||
) {
|
||||
|
||||
private val balanceWasSentMap = mutableMapOf<String, Boolean>()
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend fun send(displayedUiState: WalletState?, userWallet: UserWallet, tokenList: TokenList) {
|
||||
if (screenLifecycleProvider.isBackground) return
|
||||
if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return
|
||||
|
|
@ -34,6 +42,7 @@ internal class TokenListAnalyticsSender @Inject constructor(
|
|||
sendBalanceLoadedEventIfNeeded(tokenList.totalFiatBalance, currenciesStatuses)
|
||||
sendToppedUpEventIfNeeded(userWallet, tokenList.totalFiatBalance, currenciesStatuses)
|
||||
sendUnreachableNetworksEventIfNeeded(currenciesStatuses)
|
||||
sendTokenBalancesIfNeeded(currenciesStatuses)
|
||||
}
|
||||
|
||||
private fun getCurrenciesStatuses(tokenList: TokenList): List<CryptoCurrencyStatus> = when (tokenList) {
|
||||
|
|
@ -72,6 +81,50 @@ internal class TokenListAnalyticsSender @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun sendTokenBalancesIfNeeded(currenciesStatuses: List<CryptoCurrencyStatus>) {
|
||||
currenciesStatuses.forEach {
|
||||
val status = it.value
|
||||
if (status is CryptoCurrencyStatus.Loaded) {
|
||||
sendTokenBalancesForSpecificBlockchains(it, status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO hotfix/5.7.4 send event for log if tokens from polkadot ecosystem have balance
|
||||
private suspend fun sendTokenBalancesForSpecificBlockchains(
|
||||
currencyStatus: CryptoCurrencyStatus,
|
||||
balanceStatus: CryptoCurrencyStatus.Loaded,
|
||||
) {
|
||||
// for now send only for Polkadot ecosystem blockchains
|
||||
// later dependency on Blockchain will be removed and use token name
|
||||
when (val blockchain = Blockchain.fromNetworkId(currencyStatus.currency.network.backendId)) {
|
||||
Blockchain.Polkadot,
|
||||
Blockchain.AlephZero,
|
||||
Blockchain.Kusama,
|
||||
-> {
|
||||
if (balanceWasSentMap[blockchain.currency] != true) {
|
||||
val tokenBalance = if (balanceStatus.amount.isZero()) {
|
||||
AnalyticsParam.TokenBalanceState.Empty
|
||||
} else {
|
||||
AnalyticsParam.TokenBalanceState.Full
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
Basic.TokenBalance(
|
||||
balance = tokenBalance,
|
||||
token = blockchain.currency,
|
||||
),
|
||||
)
|
||||
mutex.withLock {
|
||||
balanceWasSentMap[blockchain.currency] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
/* no-op */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCardBalanceState(fiatBalance: TokenList.FiatBalance.Loaded): AnalyticsParam.CardBalanceState {
|
||||
return if (fiatBalance.amount > BigDecimal.ZERO) {
|
||||
AnalyticsParam.CardBalanceState.Full
|
||||
|
|
|
|||
|
|
@ -15,22 +15,22 @@ import com.tangem.domain.tokens.model.NetworkGroup
|
|||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.domain.tokens.repository.PromoRepository
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.count
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ViewModelScoped
|
||||
internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val getTokenListUseCase: GetTokenListUseCase,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||
|
|
@ -42,15 +42,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
|
||||
private var readyForRateAppNotification = false
|
||||
|
||||
fun create(clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
||||
val userWallet = getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = {
|
||||
Timber.e("Failed to get selected wallet $it")
|
||||
return flowOf(value = persistentListOf())
|
||||
},
|
||||
ifRight = { it },
|
||||
)
|
||||
|
||||
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||
|
||||
val promoFlow = flow { emit(promoRepository.getChangellyPromoBanner()) }
|
||||
|
|
|
|||
|
|
@ -9,21 +9,17 @@ import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
|
|||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ViewModelScoped
|
||||
internal class GetSingleWalletWarningsFactory @Inject constructor(
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||
|
|
@ -33,15 +29,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor(
|
|||
|
||||
private var readyForRateAppNotification = false
|
||||
|
||||
fun create(clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
||||
val userWallet = getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = {
|
||||
Timber.e("Failed to get selected wallet $it")
|
||||
return flowOf(value = persistentListOf())
|
||||
},
|
||||
ifRight = { it },
|
||||
)
|
||||
|
||||
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||
|
||||
return combine(
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ internal class MultiWalletContentLoader(
|
|||
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
|
||||
),
|
||||
MultiWalletWarningsSubscriber(
|
||||
userWalletId = userWallet.walletId,
|
||||
userWallet = userWallet,
|
||||
stateHolder = stateHolder,
|
||||
clickIntents = clickIntents,
|
||||
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ internal class SingleWalletContentLoader(
|
|||
getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase,
|
||||
),
|
||||
SingleWalletNotificationsSubscriber(
|
||||
userWalletId = userWallet.walletId,
|
||||
userWallet = userWallet,
|
||||
stateHolder = stateHolder,
|
||||
clickIntents = clickIntents,
|
||||
getSingleWalletWarningsFactory = getSingleWalletWarningsFactory,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ internal class SingleWalletWithTokenContentLoader(
|
|||
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
||||
),
|
||||
MultiWalletWarningsSubscriber(
|
||||
userWalletId = userWallet.walletId,
|
||||
userWallet = userWallet,
|
||||
stateHolder = stateHolder,
|
||||
clickIntents = clickIntents,
|
||||
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
|
||||
|
|
|
|||
|
|
@ -69,12 +69,12 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
|||
),
|
||||
)
|
||||
|
||||
object NetworksUnreachable : Warning(
|
||||
data object NetworksUnreachable : Warning(
|
||||
title = resourceReference(id = R.string.warning_network_unreachable_title),
|
||||
subtitle = resourceReference(id = R.string.warning_network_unreachable_message),
|
||||
)
|
||||
|
||||
object SomeNetworksUnreachable : Warning(
|
||||
data object SomeNetworksUnreachable : Warning(
|
||||
title = resourceReference(id = R.string.warning_some_networks_unreachable_title),
|
||||
subtitle = resourceReference(id = R.string.warning_some_networks_unreachable_message),
|
||||
)
|
||||
|
|
@ -85,7 +85,7 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
|||
onCloseClick = onCloseClick,
|
||||
)
|
||||
|
||||
object TestNetCard : Warning(
|
||||
data object TestNetCard : Warning(
|
||||
title = resourceReference(id = R.string.warning_testnet_card_title),
|
||||
subtitle = resourceReference(id = R.string.warning_testnet_card_message),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -32,19 +32,13 @@ internal sealed interface WalletState : WalletStateHolder {
|
|||
|
||||
data class Locked(
|
||||
override val walletCardState: WalletCardState,
|
||||
override val bottomSheetConfig: TangemBottomSheetConfig?,
|
||||
val onUnlockNotificationClick: () -> Unit,
|
||||
val isBottomSheetShow: Boolean = false,
|
||||
val onBottomSheetDismiss: () -> Unit = {},
|
||||
val onUnlockClick: () -> Unit,
|
||||
val onScanClick: () -> Unit,
|
||||
) : MultiCurrency(),
|
||||
WalletStateHolder by LockedWalletStateHolder(
|
||||
walletCardState,
|
||||
bottomSheetConfig,
|
||||
onUnlockNotificationClick,
|
||||
isBottomSheetShow,
|
||||
onBottomSheetDismiss,
|
||||
onUnlockClick,
|
||||
onScanClick,
|
||||
) {
|
||||
|
||||
override val tokensListState = WalletTokensListState.ContentState.Locked
|
||||
|
|
@ -70,21 +64,15 @@ internal sealed interface WalletState : WalletStateHolder {
|
|||
data class Locked(
|
||||
override val walletCardState: WalletCardState,
|
||||
override val buttons: PersistentList<WalletManageButton>,
|
||||
override val bottomSheetConfig: TangemBottomSheetConfig?,
|
||||
val onUnlockNotificationClick: () -> Unit,
|
||||
val isBottomSheetShow: Boolean = false,
|
||||
val onBottomSheetDismiss: () -> Unit = {},
|
||||
val onUnlockClick: () -> Unit,
|
||||
val onScanClick: () -> Unit,
|
||||
val onExploreClick: () -> Unit,
|
||||
) : SingleCurrency(),
|
||||
TxHistoryStateHolder by LockedTxHistoryStateHolder(onExploreClick),
|
||||
WalletStateHolder by LockedWalletStateHolder(
|
||||
walletCardState,
|
||||
bottomSheetConfig,
|
||||
onUnlockNotificationClick,
|
||||
isBottomSheetShow,
|
||||
onBottomSheetDismiss,
|
||||
onUnlockClick,
|
||||
onScanClick,
|
||||
) {
|
||||
|
||||
override val marketPriceBlockState: MarketPriceBlockState? = null
|
||||
|
|
@ -108,20 +96,14 @@ internal sealed interface WalletState : WalletStateHolder {
|
|||
data class Locked(
|
||||
override val walletCardState: WalletCardState,
|
||||
val onUnlockNotificationClick: () -> Unit,
|
||||
val isBottomSheetShow: Boolean = false,
|
||||
val onBottomSheetDismiss: () -> Unit = {},
|
||||
val onUnlockClick: () -> Unit,
|
||||
val onScanClick: () -> Unit,
|
||||
override val bottomSheetConfig: TangemBottomSheetConfig?,
|
||||
val onExploreClick: () -> Unit,
|
||||
) : Visa(),
|
||||
TxHistoryStateHolder by LockedTxHistoryStateHolder(onExploreClick),
|
||||
WalletStateHolder by LockedWalletStateHolder(
|
||||
walletCardState,
|
||||
bottomSheetConfig,
|
||||
onUnlockNotificationClick,
|
||||
isBottomSheetShow,
|
||||
onBottomSheetDismiss,
|
||||
onUnlockClick,
|
||||
onScanClick,
|
||||
) {
|
||||
|
||||
override val balancesAndLimitBlockState: BalancesAndLimitsBlockState? = null
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.model.holder
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomSheetConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletPullToRefreshConfig
|
||||
|
|
@ -18,11 +17,8 @@ internal interface WalletStateHolder {
|
|||
|
||||
internal class LockedWalletStateHolder(
|
||||
override val walletCardState: WalletCardState,
|
||||
override val bottomSheetConfig: TangemBottomSheetConfig?,
|
||||
onUnlockNotificationClick: () -> Unit,
|
||||
isBottomSheetShow: Boolean,
|
||||
onBottomSheetDismiss: () -> Unit,
|
||||
onUnlockClick: () -> Unit,
|
||||
onScanClick: () -> Unit,
|
||||
) : WalletStateHolder {
|
||||
|
||||
override val pullToRefreshConfig: WalletPullToRefreshConfig
|
||||
|
|
@ -31,13 +27,4 @@ internal class LockedWalletStateHolder(
|
|||
override val warnings: ImmutableList<WalletNotification> = persistentListOf(
|
||||
WalletNotification.UnlockWallets(onUnlockNotificationClick),
|
||||
)
|
||||
|
||||
override val bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShow = isBottomSheetShow,
|
||||
onDismissRequest = onBottomSheetDismiss,
|
||||
content = WalletBottomSheetConfig.UnlockWallets(
|
||||
onUnlockClick = onUnlockClick,
|
||||
onScanClick = onScanClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -7,18 +7,28 @@ internal class CloseBottomSheetTransformer(userWalletId: UserWalletId) : WalletS
|
|||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.MultiCurrency.Content -> {
|
||||
prevState.copy(bottomSheetConfig = prevState.bottomSheetConfig?.copy(isShow = false))
|
||||
}
|
||||
is WalletState.MultiCurrency.Locked -> prevState.copy(isBottomSheetShow = false)
|
||||
is WalletState.SingleCurrency.Content -> {
|
||||
prevState.copy(bottomSheetConfig = prevState.bottomSheetConfig?.copy(isShow = false))
|
||||
}
|
||||
is WalletState.SingleCurrency.Locked -> prevState.copy(isBottomSheetShow = false)
|
||||
is WalletState.Visa.Content -> prevState.copy(
|
||||
bottomSheetConfig = prevState.bottomSheetConfig?.copy(isShow = false),
|
||||
is WalletState.MultiCurrency.Content -> prevState.copy(
|
||||
bottomSheetConfig = updateConfig(prevState),
|
||||
)
|
||||
is WalletState.MultiCurrency.Locked -> prevState.copy(
|
||||
bottomSheetConfig = updateConfig(prevState),
|
||||
)
|
||||
is WalletState.SingleCurrency.Content -> prevState.copy(
|
||||
bottomSheetConfig = updateConfig(prevState),
|
||||
)
|
||||
is WalletState.SingleCurrency.Locked -> prevState.copy(
|
||||
bottomSheetConfig = updateConfig(prevState),
|
||||
)
|
||||
is WalletState.Visa.Content -> prevState.copy(
|
||||
bottomSheetConfig = updateConfig(prevState),
|
||||
)
|
||||
is WalletState.Visa.Locked -> prevState.copy(
|
||||
bottomSheetConfig = updateConfig(prevState),
|
||||
)
|
||||
is WalletState.Visa.Locked -> prevState.copy(isBottomSheetShow = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateConfig(prevState: WalletState) = prevState.bottomSheetConfig?.copy(
|
||||
isShow = false,
|
||||
)
|
||||
}
|
||||
|
|
@ -13,7 +13,6 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
|
||||
internal class InitializeWalletsTransformer(
|
||||
private val selectedWalletIndex: Int,
|
||||
private val selectedWallet: UserWallet,
|
||||
private val wallets: List<UserWallet>,
|
||||
private val clickIntents: WalletClickIntents,
|
||||
) : WalletScreenStateTransformer {
|
||||
|
|
@ -23,7 +22,7 @@ internal class InitializeWalletsTransformer(
|
|||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
return prevState.copy(
|
||||
onBackClick = clickIntents::onBackClick,
|
||||
topBarConfig = createTopBarConfig(userWallet = selectedWallet),
|
||||
topBarConfig = createTopBarConfig(),
|
||||
selectedWalletIndex = selectedWalletIndex,
|
||||
wallets = wallets
|
||||
.map { userWallet ->
|
||||
|
|
@ -38,13 +37,9 @@ internal class InitializeWalletsTransformer(
|
|||
)
|
||||
}
|
||||
|
||||
private fun createTopBarConfig(userWallet: UserWallet): WalletTopBarConfig {
|
||||
private fun createTopBarConfig(): WalletTopBarConfig {
|
||||
return WalletTopBarConfig(
|
||||
onDetailsClick = if (userWallet.isLocked) {
|
||||
clickIntents::onOpenUnlockWalletsBottomSheetClick
|
||||
} else {
|
||||
clickIntents::onDetailsClick
|
||||
},
|
||||
onDetailsClick = clickIntents::onDetailsClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -53,27 +48,24 @@ internal class InitializeWalletsTransformer(
|
|||
multiCurrencyCreator = {
|
||||
WalletState.MultiCurrency.Locked(
|
||||
walletCardState = userWallet.toLockedWalletCardState(),
|
||||
bottomSheetConfig = null,
|
||||
onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick,
|
||||
onUnlockClick = clickIntents::onUnlockWalletClick,
|
||||
onScanClick = clickIntents::onScanToUnlockWalletClick,
|
||||
)
|
||||
},
|
||||
singleCurrencyCreator = {
|
||||
WalletState.SingleCurrency.Locked(
|
||||
walletCardState = userWallet.toLockedWalletCardState(),
|
||||
bottomSheetConfig = null,
|
||||
buttons = createDisabledButtons(),
|
||||
onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick,
|
||||
onUnlockClick = clickIntents::onUnlockWalletClick,
|
||||
onScanClick = clickIntents::onScanToUnlockWalletClick,
|
||||
onExploreClick = clickIntents::onExploreClick,
|
||||
)
|
||||
},
|
||||
visaWalletCreator = {
|
||||
WalletState.Visa.Locked(
|
||||
walletCardState = userWallet.toLockedWalletCardState(),
|
||||
bottomSheetConfig = null,
|
||||
onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick,
|
||||
onUnlockClick = clickIntents::onUnlockWalletClick,
|
||||
onScanClick = clickIntents::onScanToUnlockWalletClick,
|
||||
onExploreClick = clickIntents::onExploreClick,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,41 +13,30 @@ internal class OpenBottomSheetTransformer(
|
|||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.MultiCurrency.Content -> {
|
||||
prevState.copy(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
onDismissRequest = onDismissBottomSheet,
|
||||
content = content,
|
||||
),
|
||||
)
|
||||
}
|
||||
is WalletState.MultiCurrency.Locked -> {
|
||||
prevState.copy(isBottomSheetShow = true, onBottomSheetDismiss = onDismissBottomSheet)
|
||||
}
|
||||
is WalletState.SingleCurrency.Content -> {
|
||||
prevState.copy(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
onDismissRequest = onDismissBottomSheet,
|
||||
content = content,
|
||||
),
|
||||
)
|
||||
}
|
||||
is WalletState.SingleCurrency.Locked -> {
|
||||
prevState.copy(isBottomSheetShow = true, onBottomSheetDismiss = onDismissBottomSheet)
|
||||
}
|
||||
is WalletState.MultiCurrency.Content -> prevState.copy(
|
||||
bottomSheetConfig = updateConfig(),
|
||||
)
|
||||
is WalletState.MultiCurrency.Locked -> prevState.copy(
|
||||
bottomSheetConfig = updateConfig(),
|
||||
)
|
||||
is WalletState.SingleCurrency.Content -> prevState.copy(
|
||||
bottomSheetConfig = updateConfig(),
|
||||
)
|
||||
is WalletState.SingleCurrency.Locked -> prevState.copy(
|
||||
bottomSheetConfig = updateConfig(),
|
||||
)
|
||||
is WalletState.Visa.Content -> prevState.copy(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
onDismissRequest = onDismissBottomSheet,
|
||||
content = content,
|
||||
),
|
||||
bottomSheetConfig = updateConfig(),
|
||||
)
|
||||
is WalletState.Visa.Locked -> prevState.copy(
|
||||
isBottomSheetShow = true,
|
||||
onBottomSheetDismiss = onDismissBottomSheet,
|
||||
bottomSheetConfig = updateConfig(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateConfig() = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
onDismissRequest = onDismissBottomSheet,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ internal class ScrollToWalletTransformer(
|
|||
private val index: Int,
|
||||
private val currentStateProvider: Provider<WalletScreenState>,
|
||||
private val stateUpdater: (WalletScreenState) -> Unit,
|
||||
private val onConsume: () -> Unit = {},
|
||||
) : WalletScreenStateTransformer {
|
||||
|
||||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
|
|
@ -23,6 +24,8 @@ internal class ScrollToWalletTransformer(
|
|||
event = consumedEvent(),
|
||||
),
|
||||
)
|
||||
|
||||
onConsume()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import com.tangem.domain.wallets.models.UserWallet
|
|||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -19,7 +18,6 @@ internal class UnlockWalletTransformer(
|
|||
|
||||
override fun transform(prevState: WalletScreenState): WalletScreenState {
|
||||
return prevState.copy(
|
||||
topBarConfig = prevState.topBarConfig.toUnlockedState(),
|
||||
wallets = prevState.wallets
|
||||
.map { state ->
|
||||
val unlockedWallet = getUnlockedWallet(state.walletCardState.id)
|
||||
|
|
@ -29,10 +27,6 @@ internal class UnlockWalletTransformer(
|
|||
)
|
||||
}
|
||||
|
||||
private fun WalletTopBarConfig.toUnlockedState(): WalletTopBarConfig {
|
||||
return copy(onDetailsClick = clickIntents::onDetailsClick)
|
||||
}
|
||||
|
||||
private fun getUnlockedWallet(walletId: UserWalletId): UserWallet? {
|
||||
return unlockedWallets.firstOrNull { it.walletId == walletId }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.domain.tokens.model.TokenActionsState
|
|||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletCurrencyActionsClickIntentsImplementor
|
||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletCurrencyActionsClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -16,7 +16,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
|
||||
internal class MultiWalletCurrencyActionsConverter(
|
||||
private val userWallet: UserWallet,
|
||||
private val clickIntents: WalletCurrencyActionsClickIntentsImplementor,
|
||||
private val clickIntents: WalletCurrencyActionsClickIntents,
|
||||
) : Converter<TokenActionsState, ImmutableList<TokenActionButtonConfig>> {
|
||||
|
||||
override fun convert(value: TokenActionsState): ImmutableList<TokenActionButtonConfig> {
|
||||
|
|
|
|||
|
|
@ -36,9 +36,11 @@ internal class MultiWalletTokenListSubscriber(
|
|||
override fun tokenListFlow(): MaybeTokenListFlow = getTokenListUseCase(userWallet.walletId)
|
||||
|
||||
override suspend fun onTokenListReceived(maybeTokenList: Either<TokenListError, TokenList>) {
|
||||
updateSortingIfNeeded(maybeTokenList)
|
||||
// TODO disabled for 5.7.2 because of potential critical
|
||||
// updateSortingIfNeeded(maybeTokenList)
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private suspend fun updateSortingIfNeeded(maybeTokenList: Either<TokenListError, TokenList>) {
|
||||
val tokenList = maybeTokenList.getOrElse { return }
|
||||
if (!checkNeedSorting(tokenList)) return
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.subscribers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
|
|
@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
internal class MultiWalletWarningsSubscriber(
|
||||
private val userWalletId: UserWalletId,
|
||||
private val userWallet: UserWallet,
|
||||
private val stateHolder: WalletStateController,
|
||||
private val clickIntents: WalletClickIntents,
|
||||
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
|
||||
|
|
@ -23,13 +23,13 @@ internal class MultiWalletWarningsSubscriber(
|
|||
) : WalletSubscriber() {
|
||||
|
||||
override fun create(coroutineScope: CoroutineScope): Flow<ImmutableList<WalletNotification>> {
|
||||
return getMultiWalletWarningsFactory.create(clickIntents)
|
||||
return getMultiWalletWarningsFactory.create(userWallet, clickIntents)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { warnings ->
|
||||
val displayedState = stateHolder.getWalletState(userWalletId)
|
||||
val displayedState = stateHolder.getWalletState(userWallet.walletId)
|
||||
|
||||
stateHolder.update(SetWarningsTransformer(userWalletId, warnings))
|
||||
stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings))
|
||||
walletWarningsAnalyticsSender.send(displayedState, warnings)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.subscribers
|
||||
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
|
|
@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class SingleWalletNotificationsSubscriber(
|
||||
private val userWalletId: UserWalletId,
|
||||
private val userWallet: UserWallet,
|
||||
private val stateHolder: WalletStateController,
|
||||
private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory,
|
||||
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
|
||||
|
|
@ -26,13 +26,13 @@ internal class SingleWalletNotificationsSubscriber(
|
|||
) : WalletSubscriber() {
|
||||
|
||||
override fun create(coroutineScope: CoroutineScope): Flow<ImmutableList<WalletNotification>> {
|
||||
return getSingleWalletWarningsFactory.create(clickIntents)
|
||||
return getSingleWalletWarningsFactory.create(userWallet, clickIntents)
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { warnings ->
|
||||
val displayedState = stateHolder.getWalletState(userWalletId)
|
||||
val displayedState = stateHolder.getWalletState(userWallet.walletId)
|
||||
|
||||
stateHolder.update(SetWarningsTransformer(userWalletId, warnings))
|
||||
stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings))
|
||||
walletWarningsAnalyticsSender.send(displayedState, warnings)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ private fun WalletContent(
|
|||
alertConfig: WalletAlertState?,
|
||||
) {
|
||||
var selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) }
|
||||
val selectedWallet = state.wallets[selectedWalletIndex]
|
||||
val selectedWallet = state.wallets.getOrElse(selectedWalletIndex) { state.wallets[state.selectedWalletIndex] }
|
||||
|
||||
val scaffoldContent: @Composable () -> Unit = {
|
||||
val movableItemModifier = Modifier.changeWalletAnimator(walletsListState)
|
||||
|
|
|
|||
|
|
@ -20,5 +20,12 @@ internal suspend fun LazyListState.animateScrollByIndex(prevIndex: Int, newIndex
|
|||
}
|
||||
|
||||
private fun calculateOffset(layoutInfo: LazyListLayoutInfo, prevIndex: Int, newIndex: Int): Float {
|
||||
return layoutInfo.viewportSize.width.times(other = newIndex - prevIndex).toFloat()
|
||||
val indexDifference = newIndex - prevIndex
|
||||
val coefficient = if (indexDifference == 0) 1 else indexDifference
|
||||
|
||||
return layoutInfo.getItemSizeWithSpacing().times(other = coefficient).toFloat()
|
||||
}
|
||||
|
||||
private fun LazyListLayoutInfo.getItemSizeWithSpacing(): Int {
|
||||
return viewportSize.width - afterContentPadding - beforeContentPadding + mainAxisItemSpacing
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import androidx.lifecycle.LifecycleOwner
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.settings.CanUseBiometryUseCase
|
||||
|
|
@ -13,7 +14,6 @@ import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase
|
|||
import com.tangem.domain.walletconnect.WalletConnectActions
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
|
||||
import com.tangem.feature.wallet.presentation.deeplink.WalletDeepLinksHandler
|
||||
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
|
||||
|
|
@ -52,7 +52,6 @@ internal class WalletViewModel @Inject constructor(
|
|||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase,
|
||||
private val canUseBiometryUseCase: CanUseBiometryUseCase,
|
||||
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
|
||||
private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled,
|
||||
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
analyticsEventsHandler: AnalyticsEventHandler,
|
||||
|
|
@ -73,7 +72,7 @@ internal class WalletViewModel @Inject constructor(
|
|||
|
||||
suggestToEnableBiometrics()
|
||||
|
||||
subscribeOnWalletsUpdateFlow()
|
||||
subscribeToUserWalletsUpdates()
|
||||
subscribeOnBalanceHiding()
|
||||
subscribeOnSelectedWalletFlow()
|
||||
}
|
||||
|
|
@ -111,25 +110,12 @@ internal class WalletViewModel @Inject constructor(
|
|||
return router.isWalletLastScreen() && shouldShowSaveWalletScreenUseCase() && canUseBiometryUseCase()
|
||||
}
|
||||
|
||||
private fun subscribeOnWalletsUpdateFlow() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
shouldSaveUserWalletsUseCase()
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.collectLatest(::subscribeToUserWalletsUpdates)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToUserWalletsUpdates(shouldSaveUserWallet: Boolean) {
|
||||
private fun subscribeToUserWalletsUpdates() {
|
||||
getWalletsUseCase()
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.map {
|
||||
walletsUpdateActionResolver.resolve(
|
||||
wallets = it,
|
||||
currentState = stateHolder.value,
|
||||
canSaveWallets = shouldSaveUserWallet,
|
||||
)
|
||||
walletsUpdateActionResolver.resolve(wallets = it, currentState = stateHolder.value)
|
||||
}
|
||||
.onEach(::updateWallets)
|
||||
.flowOn(dispatchers.main)
|
||||
|
|
@ -181,14 +167,6 @@ internal class WalletViewModel @Inject constructor(
|
|||
private suspend fun updateWallets(action: WalletsUpdateActionResolver.Action) {
|
||||
when (action) {
|
||||
is WalletsUpdateActionResolver.Action.InitializeWallets -> initializeWallets(action)
|
||||
is WalletsUpdateActionResolver.Action.ReinitializeWallets -> {
|
||||
walletScreenContentLoader.load(
|
||||
userWallet = action.selectedWallet,
|
||||
clickIntents = clickIntents,
|
||||
isRefresh = true,
|
||||
coroutineScope = viewModelScope,
|
||||
)
|
||||
}
|
||||
is WalletsUpdateActionResolver.Action.ReinitializeWallet -> reinitializeWallet(action)
|
||||
is WalletsUpdateActionResolver.Action.AddWallet -> addWallet(action)
|
||||
is WalletsUpdateActionResolver.Action.DeleteWallet -> deleteWallet(action)
|
||||
|
|
@ -199,6 +177,8 @@ internal class WalletViewModel @Inject constructor(
|
|||
is WalletsUpdateActionResolver.Action.UpdateWalletName -> {
|
||||
stateHolder.update(transformer = RenameWalletTransformer(action.selectedWalletId, action.name))
|
||||
}
|
||||
is WalletsUpdateActionResolver.Action.NoAccessibleWallets -> closeScreen(screen = AppScreen.Welcome)
|
||||
is WalletsUpdateActionResolver.Action.NoWallets -> closeScreen(screen = AppScreen.Home)
|
||||
is WalletsUpdateActionResolver.Action.Unknown -> Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -213,7 +193,6 @@ internal class WalletViewModel @Inject constructor(
|
|||
stateHolder.update(
|
||||
transformer = InitializeWalletsTransformer(
|
||||
selectedWalletIndex = action.selectedWalletIndex,
|
||||
selectedWallet = action.selectedWallet,
|
||||
wallets = action.wallets,
|
||||
clickIntents = clickIntents,
|
||||
),
|
||||
|
|
@ -249,6 +228,12 @@ internal class WalletViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) {
|
||||
walletScreenContentLoader.load(
|
||||
userWallet = action.selectedWallet,
|
||||
clickIntents = clickIntents,
|
||||
coroutineScope = viewModelScope,
|
||||
)
|
||||
|
||||
stateHolder.update(
|
||||
AddWalletTransformer(
|
||||
userWallet = action.selectedWallet,
|
||||
|
|
@ -256,12 +241,6 @@ internal class WalletViewModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
walletScreenContentLoader.load(
|
||||
userWallet = action.selectedWallet,
|
||||
clickIntents = clickIntents,
|
||||
coroutineScope = viewModelScope,
|
||||
)
|
||||
|
||||
withContext(dispatchers.io) { delay(timeMillis = 700) }
|
||||
|
||||
scrollToWallet(index = action.selectedWalletIndex)
|
||||
|
|
@ -274,23 +253,22 @@ internal class WalletViewModel @Inject constructor(
|
|||
coroutineScope = viewModelScope,
|
||||
)
|
||||
|
||||
if (action.selectedWalletIndex != 0) {
|
||||
/*
|
||||
* If card is reset to factory settings, then Compose need some time to draw the WalletScreen.
|
||||
* Otherwise, scroll isn't happened
|
||||
*/
|
||||
withContext(dispatchers.io) { delay(timeMillis = 700) }
|
||||
/*
|
||||
* If card is reset to factory settings, then Compose need some time to draw the WalletScreen.
|
||||
* Otherwise, scroll isn't happened
|
||||
*/
|
||||
withContext(dispatchers.io) { delay(timeMillis = 1000) }
|
||||
|
||||
scrollToWallet(index = action.selectedWalletIndex)
|
||||
|
||||
withContext(dispatchers.io) { delay(timeMillis = 1000) }
|
||||
}
|
||||
|
||||
stateHolder.update(
|
||||
DeleteWalletTransformer(
|
||||
selectedWalletIndex = action.selectedWalletIndex,
|
||||
deletedWalletId = action.deletedWalletId,
|
||||
),
|
||||
scrollToWallet(
|
||||
index = action.selectedWalletIndex,
|
||||
onConsume = {
|
||||
stateHolder.update(
|
||||
DeleteWalletTransformer(
|
||||
selectedWalletIndex = action.selectedWalletIndex,
|
||||
deletedWalletId = action.deletedWalletId,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -311,12 +289,20 @@ internal class WalletViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun scrollToWallet(index: Int) {
|
||||
private fun closeScreen(screen: AppScreen) {
|
||||
if (!screenLifecycleProvider.isBackground) {
|
||||
stateHolder.clear()
|
||||
router.popBackStack(screen = screen)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToWallet(index: Int, onConsume: () -> Unit = {}) {
|
||||
stateHolder.update(
|
||||
ScrollToWalletTransformer(
|
||||
index = index,
|
||||
currentStateProvider = Provider(action = stateHolder::value),
|
||||
stateUpdater = { newState -> stateHolder.update { newState } },
|
||||
onConsume = onConsume,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,21 +22,17 @@ internal class WalletsUpdateActionResolver @Inject constructor(
|
|||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
) {
|
||||
|
||||
private var isInitialized: Boolean = false
|
||||
private var canSaveWallets: Boolean = false
|
||||
fun resolve(wallets: List<UserWallet>, currentState: WalletScreenState): Action {
|
||||
val selectedWallet = wallets.getSelectedWallet()
|
||||
|
||||
fun resolve(wallets: List<UserWallet>, currentState: WalletScreenState, canSaveWallets: Boolean): Action {
|
||||
val selectedWallet = wallets.getSelectedWallet() ?: return Action.Unknown
|
||||
|
||||
val action = when {
|
||||
isFirstInitialization(currentState) -> {
|
||||
createInitializeWalletsAction(wallets, selectedWallet, canSaveWallets)
|
||||
val action = if (selectedWallet == null) {
|
||||
createNoSelectedWalletAction(wallets)
|
||||
} else {
|
||||
if (isFirstInitialization(currentState)) {
|
||||
createInitializeWalletsAction(wallets, selectedWallet)
|
||||
} else {
|
||||
getUpdateContentAction(currentState, wallets, selectedWallet)
|
||||
}
|
||||
isReinitialization(canSaveWallets) -> {
|
||||
this.canSaveWallets = canSaveWallets
|
||||
Action.ReinitializeWallets(selectedWallet = selectedWallet)
|
||||
}
|
||||
else -> getUpdateContentAction(currentState, wallets, selectedWallet)
|
||||
}
|
||||
|
||||
Timber.d("Resolved action: $action")
|
||||
|
|
@ -47,23 +43,24 @@ internal class WalletsUpdateActionResolver @Inject constructor(
|
|||
private fun List<UserWallet>.getSelectedWallet(): UserWallet? {
|
||||
return when {
|
||||
isEmpty() -> null
|
||||
size == 1 -> first()
|
||||
size == 1 -> if (first().isLocked) null else first()
|
||||
else -> getSelectedWalletSyncUseCase().fold(ifLeft = { null }, ifRight = { it })
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNoSelectedWalletAction(wallets: List<UserWallet>): Action {
|
||||
return when {
|
||||
wallets.isEmpty() -> Action.NoWallets
|
||||
wallets.all(UserWallet::isLocked) -> Action.NoAccessibleWallets
|
||||
else -> Action.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
private fun isFirstInitialization(state: WalletScreenState): Boolean {
|
||||
return state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX
|
||||
}
|
||||
|
||||
private fun createInitializeWalletsAction(
|
||||
wallets: List<UserWallet>,
|
||||
selectedWallet: UserWallet,
|
||||
canSaveWallets: Boolean,
|
||||
): Action {
|
||||
this.isInitialized = true
|
||||
this.canSaveWallets = canSaveWallets
|
||||
|
||||
private fun createInitializeWalletsAction(wallets: List<UserWallet>, selectedWallet: UserWallet): Action {
|
||||
return Action.InitializeWallets(
|
||||
selectedWalletIndex = wallets.indexOfWallet(selectedWallet.walletId),
|
||||
selectedWallet = selectedWallet,
|
||||
|
|
@ -71,10 +68,6 @@ internal class WalletsUpdateActionResolver @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun isReinitialization(canSaveWallets: Boolean): Boolean {
|
||||
return isInitialized && this.canSaveWallets != canSaveWallets
|
||||
}
|
||||
|
||||
private fun getUpdateContentAction(
|
||||
state: WalletScreenState,
|
||||
wallets: List<UserWallet>,
|
||||
|
|
@ -220,18 +213,6 @@ internal class WalletsUpdateActionResolver @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinitialize wallets. Example, if user turned on wallets saving
|
||||
*
|
||||
* @property selectedWallet selected wallet
|
||||
*/
|
||||
data class ReinitializeWallets(val selectedWallet: UserWallet) : Action() {
|
||||
|
||||
override fun toString(): String {
|
||||
return "ReinitializeWallets(selectedWallet = ${selectedWallet.walletId})"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinitialize selected wallet. Example, scanning a new card if wallets saving is turned off
|
||||
*
|
||||
|
|
@ -305,8 +286,10 @@ internal class WalletsUpdateActionResolver @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
object Unknown : Action() {
|
||||
override fun toString(): String = "Unknown"
|
||||
}
|
||||
data object NoAccessibleWallets : Action()
|
||||
|
||||
data object NoWallets : Action()
|
||||
|
||||
data object Unknown : Action()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.UpdateWalletUseCase
|
||||
|
|
@ -9,9 +8,7 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnaly
|
|||
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -76,21 +73,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
|
|||
viewModelScope.launch(dispatchers.main) {
|
||||
walletScreenContentLoader.cancel(userWalletId)
|
||||
deleteWalletUseCase(userWalletId)
|
||||
.onRight { popBackIfAllWalletsIsLocked() }
|
||||
.onLeft { Timber.e(it.toString()) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun popBackIfAllWalletsIsLocked() {
|
||||
val wallets = stateHolder.value.wallets.map(WalletState::walletCardState)
|
||||
val unlockedWallet = wallets.count { it !is WalletCardState.LockedContent }
|
||||
|
||||
if (unlockedWallet == 1) {
|
||||
stateHolder.clear()
|
||||
|
||||
router.popBackStack(
|
||||
screen = if (wallets.size > 1) AppScreen.Welcome else AppScreen.Home,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ import javax.inject.Inject
|
|||
@ViewModelScoped
|
||||
internal class WalletClickIntents @Inject constructor(
|
||||
private val walletCardClickIntentsImplementor: WalletCardClickIntentsImplementor,
|
||||
private val warningsClickIntentsImplementer: WalletWarningsClickIntentsImplementer,
|
||||
private val warningsClickIntentsImplementer: WalletWarningsClickIntentsImplementor,
|
||||
private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor,
|
||||
private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor,
|
||||
private val visaWalletIntentsImplementor: VisaWalletIntentsImplementor,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
|
||||
|
|
@ -9,17 +10,19 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.unwrap
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomSheetConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCurrencyActionsConverter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface WalletContentClickIntents {
|
||||
|
|
@ -43,8 +46,9 @@ internal interface WalletContentClickIntents {
|
|||
@ViewModelScoped
|
||||
internal class WalletContentClickIntentsImplementor @Inject constructor(
|
||||
private val stateHolder: WalletStateController,
|
||||
private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val currencyActionsClickIntents: WalletCurrencyActionsClickIntentsImplementor,
|
||||
private val walletWarningsClickIntents: WalletWarningsClickIntentsImplementor,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
|
||||
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
|
|
@ -55,7 +59,33 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
|
|||
|
||||
override fun onBackClick() = router.popBackStack()
|
||||
|
||||
override fun onDetailsClick() = router.openDetailsScreen()
|
||||
override fun onDetailsClick() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
val userWalletId = stateHolder.getSelectedWalletId()
|
||||
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
|
||||
Timber.e(
|
||||
"""
|
||||
Unable to get user wallet
|
||||
|- ID: $userWalletId
|
||||
|- Exception: $it
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (userWallet.isLocked) {
|
||||
stateHolder.showBottomSheet(
|
||||
WalletBottomSheetConfig.UnlockWallets(
|
||||
onUnlockClick = walletWarningsClickIntents::onUnlockWalletClick,
|
||||
onScanClick = walletWarningsClickIntents::onScanToUnlockWalletClick,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
router.openDetailsScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onManageTokensClick() {
|
||||
analyticsEventHandler.send(PortfolioEvent.ButtonManageTokens)
|
||||
|
|
@ -74,9 +104,20 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
|
||||
val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return
|
||||
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
val userWalletId = stateHolder.getSelectedWalletId()
|
||||
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
|
||||
Timber.e(
|
||||
"""
|
||||
Unable to get user wallet
|
||||
|- ID: $userWalletId
|
||||
|- Exception: $it
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
return@launch
|
||||
}
|
||||
|
||||
getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus)
|
||||
.take(count = 1)
|
||||
.collectLatest {
|
||||
|
|
@ -90,7 +131,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
|
|||
ActionsBottomSheetConfig(
|
||||
actions = MultiWalletCurrencyActionsConverter(
|
||||
userWallet = userWallet,
|
||||
clickIntents = currencyActionsClickIntentsImplementor,
|
||||
clickIntents = currencyActionsClickIntents,
|
||||
).convert(tokenActionsState),
|
||||
),
|
||||
userWallet.walletId,
|
||||
|
|
@ -105,9 +146,11 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
|
|||
?.currency
|
||||
?: return@launch
|
||||
|
||||
router.openUrl(
|
||||
url = getExplorerTransactionUrlUseCase(txHash = txHash, networkId = currency.network.id),
|
||||
)
|
||||
getExplorerTransactionUrlUseCase(txHash = txHash, networkId = currency.network.id)
|
||||
.fold(
|
||||
ifLeft = { Timber.e(it.toString()) },
|
||||
ifRight = { router.openUrl(url = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
|
@ -12,17 +13,19 @@ import com.tangem.domain.settings.RemindToRateAppLaterUseCase
|
|||
import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase
|
||||
import com.tangem.domain.tokens.FetchTokenListUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
|
||||
import com.tangem.domain.wallets.models.UnlockWalletsError
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletError
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.unwrap
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomSheetConfig
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
|
||||
|
|
@ -57,17 +60,17 @@ internal interface WalletWarningsClickIntents {
|
|||
|
||||
@Suppress("LongParameterList")
|
||||
@ViewModelScoped
|
||||
internal class WalletWarningsClickIntentsImplementer @Inject constructor(
|
||||
internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
||||
private val stateHolder: WalletStateController,
|
||||
private val walletEventSender: WalletEventSender,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val unlockWalletsUseCase: UnlockWalletsUseCase,
|
||||
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
private val scanCardToUnlockWalletClickHandler: ScanCardToUnlockWalletClickHandler,
|
||||
private val fetchTokenListUseCase: FetchTokenListUseCase,
|
||||
private val setCardWasScannedUseCase: SetCardWasScannedUseCase,
|
||||
private val neverToSuggestRateAppUseCase: NeverToSuggestRateAppUseCase,
|
||||
private val remindToRateAppLaterUseCase: RemindToRateAppLaterUseCase,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val scanCardToUnlockWalletClickHandler: ScanCardToUnlockWalletClickHandler,
|
||||
private val unlockWalletsUseCase: UnlockWalletsUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -82,31 +85,33 @@ internal class WalletWarningsClickIntentsImplementer @Inject constructor(
|
|||
}
|
||||
|
||||
private fun prepareOnboardingProcess() {
|
||||
getSelectedWalletSyncUseCase.unwrap()?.let {
|
||||
reduxStateHolder.dispatch(
|
||||
LegacyAction.StartOnboardingProcess(
|
||||
scanResponse = it.scanResponse,
|
||||
canSkipBackup = false,
|
||||
),
|
||||
)
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
getSelectedUserWallet()?.let {
|
||||
reduxStateHolder.dispatch(
|
||||
LegacyAction.StartOnboardingProcess(
|
||||
scanResponse = it.scanResponse,
|
||||
canSkipBackup = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCloseAlreadySignedHashesWarningClick() {
|
||||
val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return
|
||||
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
val userWallet = getSelectedUserWallet() ?: return@launch
|
||||
|
||||
setCardWasScannedUseCase(cardId = userWallet.cardId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List<CryptoCurrency>) {
|
||||
val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return
|
||||
|
||||
analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Main))
|
||||
analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped)
|
||||
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
val userWallet = getSelectedUserWallet() ?: return@launch
|
||||
|
||||
derivePublicKeysUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
currencies = missedAddressCurrencies,
|
||||
|
|
@ -119,18 +124,19 @@ internal class WalletWarningsClickIntentsImplementer @Inject constructor(
|
|||
override fun onOpenUnlockWalletsBottomSheetClick() {
|
||||
analyticsEventHandler.send(MainScreen.WalletUnlockTapped)
|
||||
|
||||
val config = requireNotNull(stateHolder.getSelectedWallet().bottomSheetConfig) {
|
||||
"Impossible to open unlock wallet bottom sheet if it's null"
|
||||
}
|
||||
|
||||
stateHolder.showBottomSheet(config.content)
|
||||
stateHolder.showBottomSheet(
|
||||
WalletBottomSheetConfig.UnlockWallets(
|
||||
onUnlockClick = this::onUnlockWalletClick,
|
||||
onScanClick = this::onScanToUnlockWalletClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onUnlockWalletClick() {
|
||||
analyticsEventHandler.send(MainScreen.UnlockAllWithBiometrics)
|
||||
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
unlockWalletsUseCase(throwIfNotAllWalletsUnlocked = true)
|
||||
unlockWalletsUseCase(type = UnlockType.ALL_WITHOUT_SELECT)
|
||||
.onRight { stateHolder.update(CloseBottomSheetTransformer(stateHolder.getSelectedWalletId())) }
|
||||
.onLeft(::handleUnlockWalletsError)
|
||||
}
|
||||
|
|
@ -204,4 +210,19 @@ internal class WalletWarningsClickIntentsImplementer @Inject constructor(
|
|||
shouldShowSwapPromoWalletUseCase.neverToShow()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getSelectedUserWallet(): UserWallet? {
|
||||
val userWalletId = stateHolder.getSelectedWalletId()
|
||||
return getUserWalletUseCase(userWalletId).getOrElse {
|
||||
Timber.e(
|
||||
"""
|
||||
Unable to get user wallet
|
||||
|- ID: $userWalletId
|
||||
|- Exception: $it
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -85,9 +85,9 @@ leakcanary = "2.13"
|
|||
# endregion Other libraries
|
||||
|
||||
# region Tangem
|
||||
tangemBlockchainSdk = "develop-537"
|
||||
tangemBlockchainSdk = "develop-538"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "develop-332"
|
||||
tangemCardSdk = "develop-337"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
# endregion Tangem
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue