diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index 14327b84be..b638128b34 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -610,7 +610,7 @@ ] }, { - "id": "aurora-near", + "id": "aurora-ethereum", "name": "Aurora Testnet", "symbol": "ETH", "networks": [ diff --git a/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt b/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt index 08f62eee72..c56c238e57 100644 --- a/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt +++ b/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index d9e620497a..94b72fcf1e 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index 1a162862e6..f2e03181e1 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt index efb44252c2..51bdd56ae6 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index a27e52d22e..160d433012 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -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 { - return unlockWithBiometryInternal() + override suspend fun unlock(type: UnlockType): CompletionResult { + 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 { - 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 { - 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 { 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 { - 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 { + 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> { + private suspend fun loadUserWallets(encryptionKeys: List): CompletionResult { 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, - ): 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) { diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt index cf7497ea51..e0e160a524 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt @@ -100,10 +100,10 @@ internal class GeneralUserWalletsListManager( return implementation.value.get(userWalletId) } - override suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean): CompletionResult { + override suspend fun unlock(type: UserWalletsListManager.Lockable.UnlockType): CompletionResult { 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() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt index 3c548bd255..7e7a781189 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt @@ -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 { - 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): Map { - return keystoreManagerProvider().get(keyAliases) + override suspend fun get( + masterKeyConfig: KeystoreManager.MasterKeyConfig, + keyAliases: Set, + ): Map { + 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) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt index 0b06c9565f..6f7c82d446 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/SupportBlockchainType.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/SupportBlockchainType.kt new file mode 100644 index 0000000000..27215046bd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/SupportBlockchainType.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.features.customtoken.impl.presentation.models + +internal enum class SupportBlockchainType { + + SUPPORTED, UNSUPPORTED, UNABLE_TO_DETERMINE +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/CustomTokenRouter.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/CustomTokenRouter.kt index 4444aeea64..1c016c4993 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/CustomTokenRouter.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/CustomTokenRouter.kt @@ -20,4 +20,6 @@ internal interface CustomTokenRouter { * @param blockchain blockchain to show alert */ fun openUnsupportedNetworkAlert(blockchain: Blockchain) + + fun showGenericErrorAlertAndPopBack() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt index af1ebb8795..6391cc840f 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt @@ -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) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 4efb2848fc..3dd2804b29 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -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()) { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 79104e0170..8453733f19 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt index 934f7e8880..e5625ad1d2 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt @@ -11,20 +11,20 @@ internal sealed interface SaveWalletAction : Action { val backupCardsIds: Set?, ) : 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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index ddf1c22d74..344a0909de 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt index 15f39a8914..27418f3d01 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/TokensListRouter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/TokensListRouter.kt index 8347f59a2e..34b5379b48 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/TokensListRouter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/TokensListRouter.kt @@ -36,6 +36,8 @@ internal interface TokensListRouter { */ fun openUnsupportedNetworkAlert(blockchain: Blockchain) + fun showGenericErrorAlertAndPopBack() + /** * Open alert with unsupported networks tokens error */ diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokensListStateHolder.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokensListStateHolder.kt index 101cdc8f52..bf48a93269 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokensListStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/states/TokensListStateHolder.kt @@ -96,5 +96,6 @@ internal sealed interface TokensListStateHolder { override val tokens: Flow>, override val onTokensLoadStateChanged: (LoadState) -> Unit, val onSaveButtonClick: () -> Unit, + val isSavingInProgress: Boolean, ) : TokensListStateHolder } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt index 3dff613b36..70335b9609 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt @@ -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) { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index 0853b72b8f..0fa1976240 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -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 + } }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 5e03eed1a5..ca7d00d461 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -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)) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index a684154112..5f53ab40d7 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -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" diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt index e9b2317f35..f67f25584f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt @@ -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( private val adapter: JsonAdapter, ) : StringKeyDataStore { + 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( override fun get(key: String): Flow { return writeTrigger - .map { getInternal(key) } + .map { + mutex.withLock { + getInternal(key) + } + } .filterNotNull() .distinctUntilChanged() } @@ -53,10 +60,12 @@ internal class FileDataStore( 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") } diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index ac9325b011..ce4c2d9448 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -13,7 +13,7 @@ }, { "name": "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED", - "version": "5.7.0" + "version": "5.8.0" }, { "name": "LOCAL_USER_LOGS_ENABLED", diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index 9b56a22f57..3d0b87e26a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -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), diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt index 6d6f19cd0c..d0938c6718 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt @@ -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() + 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 + } } } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index fc4296fdb8..44096bdcd7 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -56,8 +56,9 @@ internal class DefaultNetworksRepository( .cancellable() override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { + val currencies = getCurrencies(userWalletId, networks) withContext(dispatchers.io) { - fetchNetworksPendingTransactions(userWalletId, networks) + fetchNetworksPendingTransactions(userWalletId, networks, currencies) } } @@ -80,23 +81,28 @@ internal class DefaultNetworksRepository( networks: Set, 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) { + private suspend fun fetchNetworksPendingTransactions( + userWalletId: UserWalletId, + networks: Set, + currencies: Sequence, + ) { 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, 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, + ) { val result = walletManagersFacade.update( userWalletId = userWalletId, network = network, - extraTokens = currencies.filterIsInstance().toSet(), + extraTokens = currencies + .filterIsInstance() + .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, + ) { 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 { + private suspend fun getCurrencies(userWalletId: UserWalletId, networks: Set): Sequence { 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( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index bd834c5450..d362189832 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -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): Flow> { @@ -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, appCurrencyId: String) { @@ -118,7 +124,6 @@ internal class DefaultQuotesRepository( ): Set { return currenciesIds.fold(hashSetOf()) { acc, currencyId -> val rawCurrencyId = currencyId.rawCurrencyId - if (rawCurrencyId != null && rawCurrencyId !in acc) { cacheRegistry.invokeOnExpire( key = getQuoteCacheKey(rawCurrencyId), diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt index a038b0be78..e6d573b353 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt @@ -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", ) } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 3ee891252a..fdbe2e0fac 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -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 diff --git a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxStatusError.kt b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxStatusError.kt new file mode 100644 index 0000000000..51a367f46f --- /dev/null +++ b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxStatusError.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.txhistory.models + +sealed class TxStatusError { + + data object EmptyUrlError : TxStatusError() + data class DataError(val cause: Throwable) : TxStatusError() +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt index d5628aa838..974d439c7e 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt @@ -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 { + return either { + catch( + block = { + repository.getTxExploreUrl(txHash, networkId).ifEmpty { + raise(TxStatusError.EmptyUrlError) + } + }, + catch = { raise(TxStatusError.DataError(it)) }, + ) + } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt index 7211f73a66..8a63233bed 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt @@ -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 + suspend fun unlock(type: UnlockType): CompletionResult /** 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 diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt index aa65450d99..0003a1300d 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt @@ -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 { - return asLockable()?.unlock() ?: CompletionResult.Failure(UserWalletsListError.UnableToUnlockUserWallets()) +suspend fun UserWalletsListManager.unlockIfLockable(type: UnlockType = UnlockType.ANY): CompletionResult { + return asLockable()?.unlock(type) ?: CompletionResult.Failure(UserWalletsListError.UnableToUnlockUserWallets()) } /** diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt index ae2ace600a..2b9c40ae27 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.flow.firstOrNull class GetUserWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { - suspend operator fun invoke(userWalletId: UserWalletId): Either = either { + suspend operator fun invoke(userWalletId: UserWalletId): Either = either { val userWalletsListManager = ensureUserWalletListManagerNotNull( walletsStateHolder = walletsStateHolder, raise = GetUserWalletError::DataError, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt index 5ae878661a..9b6bdf4997 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt @@ -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 = - 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 = 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) + } + } } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt index 41a8b85226..577380dc6b 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt @@ -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() 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() diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt index a4ca7198dc..6edcbe2ea2 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt @@ -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) }, ) diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt index 231081714b..4320634f4d 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt @@ -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()) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 3d28d8ae4a..9b523c44f2 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -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(), diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt index 30ea8ec4cd..e3c5321be7 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt @@ -14,6 +14,7 @@ internal class ExchangeStatusConverter : Converter - 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() .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( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 8d89cd4924..fc73c5029a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -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, ), ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 2077c47764..90e9ca9b8c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -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) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index d705013208..a8a0978de4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -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() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index 0b2b604294..495f7edd82 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -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( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index 563ab70f1e..655a49a92b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -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() + 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 = when (tokenList) { @@ -72,6 +81,50 @@ internal class TokenListAnalyticsSender @Inject constructor( } } + private suspend fun sendTokenBalancesIfNeeded(currenciesStatuses: List) { + 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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 9fceb87f55..7432cbb84e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -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> { - 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> { val cardTypesResolver = userWallet.scanResponse.cardTypesResolver val promoFlow = flow { emit(promoRepository.getChangellyPromoBanner()) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt index edcbb820d8..c502e65890 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -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> { - 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> { val cardTypesResolver = userWallet.scanResponse.cardTypesResolver return combine( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index b12083df95..ba746cd830 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -44,7 +44,7 @@ internal class MultiWalletContentLoader( applyTokenListSortingUseCase = applyTokenListSortingUseCase, ), MultiWalletWarningsSubscriber( - userWalletId = userWallet.walletId, + userWallet = userWallet, stateHolder = stateHolder, clickIntents = clickIntents, getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt index 45b3b831a5..b4c0f3252e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt @@ -49,7 +49,7 @@ internal class SingleWalletContentLoader( getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, ), SingleWalletNotificationsSubscriber( - userWalletId = userWallet.walletId, + userWallet = userWallet, stateHolder = stateHolder, clickIntents = clickIntents, getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index 42e26f47f2..525f4952ae 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -38,7 +38,7 @@ internal class SingleWalletWithTokenContentLoader( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, ), MultiWalletWarningsSubscriber( - userWalletId = userWallet.walletId, + userWallet = userWallet, stateHolder = stateHolder, clickIntents = clickIntents, getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index da66d2d54c..9f7f5ea139 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -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), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 0792d8f25a..81c9017dee 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -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, + 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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt index 6c361d5da7..96bef56446 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/holder/LockedWalletStateHolder.kt @@ -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 = persistentListOf( WalletNotification.UnlockWallets(onUnlockNotificationClick), ) - - override val bottomSheetConfig = TangemBottomSheetConfig( - isShow = isBottomSheetShow, - onDismissRequest = onBottomSheetDismiss, - content = WalletBottomSheetConfig.UnlockWallets( - onUnlockClick = onUnlockClick, - onScanClick = onScanClick, - ), - ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt index 81106b0db4..02dd3d93aa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt @@ -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, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index 7b94f4d770..73065cb197 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -13,7 +13,6 @@ import kotlinx.collections.immutable.toImmutableList internal class InitializeWalletsTransformer( private val selectedWalletIndex: Int, - private val selectedWallet: UserWallet, private val wallets: List, 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, ) }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt index 141f4a57d0..69417c564c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt @@ -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, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ScrollToWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ScrollToWalletTransformer.kt index 6381c98d9d..d7135f0fdf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ScrollToWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ScrollToWalletTransformer.kt @@ -10,6 +10,7 @@ internal class ScrollToWalletTransformer( private val index: Int, private val currentStateProvider: Provider, 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() }, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index f22dedbc22..be16388c83 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -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 } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index 16876a9396..0e76d03e34 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -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> { override fun convert(value: TokenActionsState): ImmutableList { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index 1c53ebee84..9185370945 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -36,9 +36,11 @@ internal class MultiWalletTokenListSubscriber( override fun tokenListFlow(): MaybeTokenListFlow = getTokenListUseCase(userWallet.walletId) override suspend fun onTokenListReceived(maybeTokenList: Either) { - updateSortingIfNeeded(maybeTokenList) + // TODO disabled for 5.7.2 because of potential critical + // updateSortingIfNeeded(maybeTokenList) } + @Suppress("UnusedPrivateMember") private suspend fun updateSortingIfNeeded(maybeTokenList: Either) { val tokenList = maybeTokenList.getOrElse { return } if (!checkNeedSorting(tokenList)) return diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt index d9029393c1..cf18d05d6d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt @@ -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> { - 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) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt index f266b681fd..a1ef1d4ee6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt @@ -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> { - 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) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 9d6dc72199..9819f09790 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -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) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt index a9ccd192d4..d1b702c3b9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt @@ -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 } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 62d6a03cfd..65ca30c4ff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -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, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt index bdfd27558d..012aee200d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -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, currentState: WalletScreenState): Action { + val selectedWallet = wallets.getSelectedWallet() - fun resolve(wallets: List, 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.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): 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, - selectedWallet: UserWallet, - canSaveWallets: Boolean, - ): Action { - this.isInitialized = true - this.canSaveWallets = canSaveWallets - + private fun createInitializeWalletsAction(wallets: List, 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, @@ -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() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index fcbbdab810..7e47a2a490 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -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, - ) - } - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index c27ff4424e..3cf99441ab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -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, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index 3ebb1b4830..cae3373a31 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -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) }, + ) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 5f6133df1b..f16f13dda2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -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) { - 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 + } + } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index a91c4340a5..df3dce1313 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -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