diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a15171a939..1876b0038c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -198,6 +198,7 @@ dependencies { implementation(Library.armadillo) implementation(Library.googlePlayServicesWallet) implementation(Library.composeShimmer) + implementation(Library.mviCoreWatcher) /** Testing libraries */ testImplementation(Test.junit) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e8a5618a92..e7d11764b4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -101,6 +101,17 @@ + + + + + + + + + + + diff --git a/app/src/main/java/com/tangem/tap/common/IntentHandler.kt b/app/src/main/java/com/tangem/tap/common/IntentHandler.kt index cfee84fd69..4ac3547aa0 100644 --- a/app/src/main/java/com/tangem/tap/common/IntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/IntentHandler.kt @@ -3,6 +3,7 @@ package com.tangem.tap.common import android.content.Intent import android.nfc.NfcAdapter import android.nfc.Tag +import com.tangem.tap.common.extensions.removePrefixOrNull import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.walletconnect.WalletConnectManager @@ -14,14 +15,20 @@ import timber.log.Timber class IntentHandler { - private val TRANSACTION_ID_PARAM = "transactionId" - private val CURRENCY_CODE_PARAM = "baseCurrencyCode" - private val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount" - private val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress" - fun handleWalletConnectLink(intent: Intent?) { - if (intent?.scheme == WalletConnectManager.WC_SCHEME) { - store.dispatch(WalletConnectAction.HandleDeepLink(intent.data?.toString())) + val wcUri = when (intent?.scheme) { + WalletConnectManager.WC_SCHEME -> { + intent.data?.toString() + } + TANGEM_SCHEME -> { + intent.data?.toString()?.removePrefixOrNull(TANGEM_WC_PREFIX) + } + else -> { + null + } + } + if (wcUri != null) { + store.dispatch(WalletConnectAction.HandleDeepLink(wcUri)) } } @@ -54,16 +61,25 @@ class IntentHandler { Timber.d("MoonPay Sell: $amount $currency to $destinationAddress") - store.dispatch(WalletAction.TradeCryptoAction.SendCrypto( - currencyId = currency, - amount = amount, - destinationAddress = destinationAddress, - transactionId = transactionID - )) + store.dispatch( + WalletAction.TradeCryptoAction.SendCrypto( + currencyId = currency, + amount = amount, + destinationAddress = destinationAddress, + transactionId = transactionID, + ), + ) } catch (exception: Exception) { Timber.d("Not MoonPay URL") } } - + companion object { + private const val TRANSACTION_ID_PARAM = "transactionId" + private const val CURRENCY_CODE_PARAM = "baseCurrencyCode" + private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount" + private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress" + private const val TANGEM_SCHEME = "tangem" + private const val TANGEM_WC_PREFIX = "tangem://wc?uri=" + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt index 301ae0cb07..45d55aac2a 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt @@ -25,7 +25,7 @@ class BasicSignInEventConverter( val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse) ?: return null return Basic.SignedIn( - state = AnalyticsParam.CardBalanceState.from(value.walletsData), + state = AnalyticsParam.CardBalanceState.from(value.walletsDataFromStores), currency = cardCurrency, batch = scanResponse.card.batchId, ).apply { @@ -44,7 +44,7 @@ class BasicTopUpEventConverter( val data = BasicTopUpFilter.Data( walletId = scanResponse.card.userWalletId.stringValue, - cardBalanceState = AnalyticsParam.CardBalanceState.from(value.walletsData), + cardBalanceState = AnalyticsParam.CardBalanceState.from(value.walletsDataFromStores), ) return Basic.ToppedUp(cardCurrency).apply { filterData = data } @@ -61,16 +61,16 @@ private fun AnalyticsParam.CardBalanceState.Companion.from(walletsData: List acc + i } - if (balancesCount != state.wallets.size) return false + if (balancesCount != state.walletsStores.size) return false return true } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/String.kt b/app/src/main/java/com/tangem/tap/common/extensions/String.kt index 7d54a7bf92..021cc3a7f2 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/String.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/String.kt @@ -62,4 +62,9 @@ fun String.toQrCode(): Bitmap { return bmp } -fun String.urlEncode(): String = Uri.encode(this) \ No newline at end of file +fun String.urlEncode(): String = Uri.encode(this) + +fun String.removePrefixOrNull(prefix: String): String? = when { + startsWith(prefix) -> substring(prefix.length) + else -> null +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt new file mode 100644 index 0000000000..e9aaabfd70 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.common.redux + +import com.tangem.domain.common.ScanResponse +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.preferencesStorage +import com.tangem.tap.tangemSdkManager +import org.rekotlin.Middleware + +class AccessCodeRequestPolicyMiddleware { + val middleware: Middleware = { _, _ -> + { next -> + { action -> + if (action is GlobalAction.SaveScanResponse) { + updateAccessCodeRequestPolicy(action.scanResponse) + } + next(action) + } + } + } + + private fun updateAccessCodeRequestPolicy(scanResponse: ScanResponse) { + tangemSdkManager.setAccessCodeRequestPolicy( + useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes && + scanResponse.card.isAccessCodeSet, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index fad0770f65..9f7c34b90f 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -93,6 +93,7 @@ data class AppState( SaveWalletMiddleware().middleware, WalletSelectorMiddleware().middleware, LockUserWalletsTimerMiddleware().middleware, + AccessCodeRequestPolicyMiddleware().middleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index d4c5620fb3..1f51374cae 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -64,7 +64,7 @@ sealed class GlobalAction : Action { object Increment : GlobalAction() } - data class SaveScanNoteResponse(val scanResponse: ScanResponse) : GlobalAction() + data class SaveScanResponse(val scanResponse: ScanResponse) : GlobalAction() data class SetIfCardVerifiedOnline(val verified: Boolean) : GlobalAction() diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index f4aa80ad03..543e734f8f 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -33,7 +33,7 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde is GlobalAction.ScanFailsCounter.Reset -> { globalState.copy(scanCardFailsCounter = 0) } - is GlobalAction.SaveScanNoteResponse -> { + is GlobalAction.SaveScanResponse -> { appStateHolder.scanResponse = action.scanResponse domainStore.dispatch(DomainGlobalAction.SaveScanNoteResponse(action.scanResponse)) globalState.copy(scanResponse = action.scanResponse) diff --git a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt index 271886dea1..808cb5bbbc 100644 --- a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt +++ b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt @@ -208,8 +208,8 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) { } companion object { - const val TANGEM_WALLET_2_CARDS_SKU = "TG115x2" - const val TANGEM_WALLET_3_CARDS_SKU = "TG115x3" + const val TANGEM_WALLET_2_CARDS_SKU = "TG115X2-S" + const val TANGEM_WALLET_3_CARDS_SKU = "TG115X3-S" val SKUS_TO_DISPLAY = listOf(TANGEM_WALLET_2_CARDS_SKU, TANGEM_WALLET_3_CARDS_SKU) } } 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 81ddf880c3..01350c6c63 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -46,6 +46,14 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) { + + private val userCodeRepository by lazy { + UserCodeRepository( + biometricManager = tangemSdk.biometricManager, + secureStorage = tangemSdk.secureStorage, + ) + } + val canUseBiometry: Boolean get() = tangemSdk.biometricManager.canAuthenticate || needEnrollBiometrics @@ -119,8 +127,8 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult { - return createUserCodeRepository().save( - cardIds = cardsIds, + return userCodeRepository.save( + cardsIds = cardsIds, userCode = UserCode( type = UserCodeType.AccessCode, stringValue = accessCode, @@ -128,8 +136,12 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co ) } + suspend fun deleteSavedUserCodes(cardsIds: Set): CompletionResult { + return userCodeRepository.delete(cardsIds.toSet()) + } + suspend fun clearSavedUserCodes(): CompletionResult { - return createUserCodeRepository().clear() + return userCodeRepository.clear() } suspend fun setPasscode(cardId: String?): CompletionResult { @@ -166,9 +178,10 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co suspend fun scanCard( cardId: String? = null, + allowRequestAccessCodeFromRepository: Boolean = false, ): CompletionResult { return runTaskAsyncReturnOnMain( - runnable = ScanTask(), + runnable = ScanTask(allowRequestAccessCodeFromRepository), cardId = cardId, initialMessage = Message(context.getString(R.string.initial_message_tap_header)), ) @@ -221,13 +234,6 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } } - private fun createUserCodeRepository() = with(tangemSdk) { - UserCodeRepository( - biometricManager = biometricManager, - secureStorage = secureStorage, - ) - } - companion object { val config = Config( linkedTerminal = true, diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 7eeb396463..ea26d15848 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -33,6 +33,7 @@ import com.tangem.tap.features.wallet.models.toBlockchainNetworks import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import com.tangem.tap.userTokensRepository import com.tangem.tap.walletStoresManager import kotlinx.coroutines.Dispatchers @@ -94,6 +95,7 @@ class TapWalletManager { val card = scanResponse.card val attestationFailed = card.attestation.status == Attestation.Status.Failed + tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse) updateConfigManager(scanResponse) @@ -101,7 +103,7 @@ class TapWalletManager { store.dispatch(WalletAction.UserWalletChanged(userWallet)) store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) store.dispatch(WalletConnectAction.ResetState) - store.dispatch(GlobalAction.SaveScanNoteResponse(scanResponse)) + store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) store.dispatch(WalletConnectAction.RestoreSessions(scanResponse)) store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed)) store.dispatch(WalletAction.Warnings.CheckIfNeeded) @@ -149,7 +151,7 @@ class TapWalletManager { withMainContext { store.dispatch(WalletAction.ResetState(data.card)) store.dispatch(WalletConnectAction.ResetState) - store.dispatch(GlobalAction.SaveScanNoteResponse(data)) + store.dispatch(GlobalAction.SaveScanResponse(data)) store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard)) store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed)) store.dispatch(WalletConnectAction.RestoreSessions(data)) diff --git a/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt b/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt index ac3ac6d56a..dd9a0b27a1 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt @@ -11,10 +11,6 @@ sealed class TotalFiatBalance { object Loading : TotalFiatBalance() - data class Refreshing( - override val amount: BigDecimal, - ) : TotalFiatBalance() - data class Error( override val amount: BigDecimal, ) : TotalFiatBalance() diff --git a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt index 8c3403ede9..4dec0df7d1 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt @@ -6,22 +6,23 @@ import com.tangem.domain.common.util.UserWalletId /** * Represents user's wallet which stored in app persistence - * @param name User's wallet name - * @param walletId User's wallet [UserWalletId] + * @param name User wallet name + * @param walletId User wallet [UserWalletId] * @param artworkUrl User wallet card artwork URL * @param cardsInWallet List of cards IDs assigned with this user's wallet + * @param isMultiCurrency Indicates whether this user wallet can work with more than one currency * @param scanResponse [ScanResponse] of primary user's wallet card. * TODO: Replace with [com.tangem.domain.common.CardDTO] * @property cardId ID of user's wallet primary card * @property hasAccessCode Indicates if the user's wallet primary card has access code * @property isLocked Indicates if this primary card has no currency wallets - * @property isSaved Indicates if this user wallet is saved * */ data class UserWallet( val name: String, val walletId: UserWalletId, val artworkUrl: String, val cardsInWallet: Set, + val isMultiCurrency: Boolean, val scanResponse: ScanResponse, ) { val cardId: String @@ -32,17 +33,18 @@ data class UserWallet( val isLocked: Boolean get() = scanResponse.card.wallets.isEmpty() - - internal var isSaved: Boolean = true } /** * !!! Workaround !!! * * Calculate same [UserWalletId] for twins instead + * + * TODO: Remove after [REDACTED_JIRA] * */ fun UserWallet.isTwinnedWith(other: UserWallet): Boolean { - if (!scanResponse.isTangemTwins()) return false + if (!scanResponse.isTangemTwins() || !other.scanResponse.isTangemTwins()) return false + if (scanResponse.secondTwinPublicKey == null || other.scanResponse.secondTwinPublicKey == null) return false if (other.scanResponse.secondTwinPublicKey == scanResponse.card.wallets.firstOrNull()?.publicKey?.toHexString()) { return true } diff --git a/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt b/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt index 439b900c39..4462f4d8ec 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt @@ -35,12 +35,6 @@ data class WalletDataModel( open val pendingTransactions: List = emptyList() open val errorMessage: String? = null open val isErrorStatus: Boolean = false - - fun asRefreshing() = Refreshing( - amount = amount, - pendingTransactions = pendingTransactions, - errorMessage = errorMessage, - ) } object Loading : Status() @@ -74,12 +68,4 @@ data class WalletDataModel( object MissedDerivation : Status() { override val isErrorStatus: Boolean = true } - - data class Refreshing( - override val amount: BigDecimal, - override val pendingTransactions: List, - override val errorMessage: String?, - ) : Status() { - override val isErrorStatus: Boolean = errorMessage != null - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt b/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt index cba89801a3..a0511784e2 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt @@ -1,6 +1,8 @@ package com.tangem.tap.domain.model +import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager +import com.tangem.common.hdWallet.DerivationPath import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.domain.model.WalletStoreModel.WalletRent import com.tangem.tap.domain.tokens.models.BlockchainNetwork @@ -8,21 +10,28 @@ import java.math.BigDecimal /** * Contains info about the blockchain and its currencies - * @param userWalletId ID of the [UserWallet] which uses that store - * @param blockchainNetwork Store's [BlockchainNetwork] - * @param walletManager Store's [WalletManager], may be null if it fails to create this manager. TODO: Remove after - * WalletMiddleware refactoring + * @param userWalletId ID of the associated [UserWallet] + * @param blockchain [Blockchain] of this WalletStore + * @param derivationPath [DerivationPath] of this store, null if the card does not support the + * [HD Wallet](https://coinsutra.com/hd-wallets-deterministic-wallet/) * @param walletsData List of [WalletDataModel] which represents store's blockchain currency and tokens currencies - * @param walletRent Store's [WalletRent], null if store has no rent or currency balance is greater then + * @param walletRent [WalletRent], null if store has no rent or currency balance is greater then * [WalletRent.exemptionAmount] + * @param blockchainNetwork [BlockchainNetwork]. + * TODO: Remove after WalletMiddleware refactoring + * @param walletManager [WalletManager], may be null if it fails to create this manager. + * TODO: Remove after WalletMiddleware refactoring * */ data class WalletStoreModel( val userWalletId: UserWalletId, + val blockchain: Blockchain, + val derivationPath: DerivationPath?, + val walletsData: List, + val walletRent: WalletRent?, + @Deprecated("Don't use it, will be removed") val blockchainNetwork: BlockchainNetwork, @Deprecated("Don't use it, will be removed") val walletManager: WalletManager?, - val walletsData: List, - val walletRent: WalletRent?, ) { /** @@ -35,4 +44,34 @@ data class WalletStoreModel( val rent: BigDecimal, val exemptionAmount: BigDecimal, ) + + // TODO: Remove the generated methods after blockchainNetwork and walletManager are removed from the model + // region Generated + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is WalletStoreModel) return false + + if (userWalletId != other.userWalletId) return false + if (blockchain != other.blockchain) return false + if (derivationPath != other.derivationPath) return false + if (walletsData != other.walletsData) return false + if (walletRent != other.walletRent) return false + + return true + } + + override fun hashCode(): Int { + var result = userWalletId.hashCode() + result = 31 * result + blockchain.hashCode() + result = 31 * result + (derivationPath?.hashCode() ?: 0) + result = 31 * result + walletsData.hashCode() + result = 31 * result + (walletRent?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "WalletStoreModel(userWalletId=$userWalletId, blockchain=$blockchain, derivationPath=$derivationPath, " + + "walletsData=$walletsData, walletRent=$walletRent)" + } + // endregion Generated } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt index 377b8cf8c6..6e4d834d38 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt @@ -1,15 +1,48 @@ package com.tangem.tap.domain.model.builders +import com.tangem.common.extensions.toHexString +import com.tangem.common.services.Result +import com.tangem.domain.common.CardDTO +import com.tangem.domain.common.ProductType import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.TwinCardNumber +import com.tangem.domain.common.TwinsHelper import com.tangem.domain.common.util.userWalletId -import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl +import com.tangem.operations.attestation.OnlineCardVerifier +import com.tangem.operations.attestation.TangemApi import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.features.wallet.redux.Artwork class UserWalletBuilder( private val scanResponse: ScanResponse, + private val onlineCardVerifier: OnlineCardVerifier = OnlineCardVerifier(), ) { private var backupCardsIds: Set = emptySet() + private val CardDTO.isBackupNotAllowed: Boolean + get() = !this.settings.isBackupAllowed + + private val ScanResponse.userWalletName: String + get() = when (productType) { + ProductType.Note -> "Note" + ProductType.Twins -> "Twin" + ProductType.SaltPay -> "SaltPay" + ProductType.Wallet -> when { + card.isBackupNotAllowed -> "Tangem card" + card.isStart2Coin -> "Start2Coin" + else -> "Wallet" + } + } + + private val ScanResponse.isMultiCurrency: Boolean + get() = when (productType) { + ProductType.Note -> false + ProductType.Twins -> false + ProductType.SaltPay -> false + ProductType.Wallet -> !card.isStart2Coin + } + fun backupCardsIds(backupCardsIds: Set?) = this.apply { if (backupCardsIds != null) { this.backupCardsIds = backupCardsIds @@ -20,11 +53,44 @@ class UserWalletBuilder( return with(scanResponse) { UserWallet( walletId = card.userWalletId, - name = productType.name, - artworkUrl = card.getOrLoadCardArtworkUrl(), + name = userWalletName, + artworkUrl = loadArtworkUrl(card.cardId, card.cardPublicKey), cardsInWallet = backupCardsIds.plus(card.cardId), scanResponse = this, + isMultiCurrency = isMultiCurrency, ) } } + + private suspend fun loadArtworkUrl(cardId: String, cardPublicKey: ByteArray): String { + return when (val result = onlineCardVerifier.getCardInfo(cardId, cardPublicKey)) { + is Result.Success -> { + val artworkId = result.data.artwork?.id + if (artworkId.isNullOrEmpty()) { + getFallbackArtworkUrl(cardId) + } else { + getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId) + } + } + + is Result.Failure -> getFallbackArtworkUrl(cardId) + } + } + + private fun getFallbackArtworkUrl(cardId: String): String { + return when { + cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL + cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL + else -> when (TwinsHelper.getTwinCardNumber(cardId)) { + TwinCardNumber.First -> Artwork.TWIN_CARD_1 + TwinCardNumber.Second -> Artwork.TWIN_CARD_2 + else -> Artwork.DEFAULT_IMG_URL + } + } + } + + private fun getUrlForArtwork(cardId: String, cardPublicKeyHex: String, artworkId: String): String { + return TangemApi.Companion.BaseUrl.VERIFY.url + TangemApi.ARTWORK + + "?artworkId=${artworkId}&CID=${cardId}&publicKey=$cardPublicKeyHex" + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt index f41c664276..e76a291668 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.WalletManager +import com.tangem.common.hdWallet.DerivationPath import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.domain.model.WalletStoreModel @@ -54,10 +55,12 @@ private class BlockchainNetworkWalletStoreBuilderImpl( return WalletStoreModel( userWalletId = userWalletId, - blockchainNetwork = blockchainNetwork, - walletManager = walletManager, + blockchain = blockchainNetwork.blockchain, + derivationPath = blockchainNetwork.derivationPath?.let { DerivationPath(it) }, walletsData = (listOf(blockchainWalletData) + tokensWalletsData), walletRent = null, + walletManager = walletManager, + blockchainNetwork = blockchainNetwork, ) } } @@ -74,10 +77,12 @@ private class WalletMangerWalletStoreBuilderImpl( return WalletStoreModel( userWalletId = userWalletId, - blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager), - walletManager = walletManager, + blockchain = wallet.blockchain, + derivationPath = wallet.publicKey.derivationPath, walletsData = (listOf(blockchainWalletData) + listOfNotNull(tokenWalletsData)), walletRent = null, + walletManager = walletManager, + blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager), ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt index 6ab45894b9..d940a338ad 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt @@ -82,6 +82,7 @@ object ScanCardProcessor { nextHandler = { scanResponse1 -> showDisclaimerIfNeed( scanResponse = scanResponse1, + onProgressStateChange = onProgressStateChange, nextHandler = { scanResponse2 -> onScanSuccess( scanResponse = scanResponse2, @@ -127,6 +128,7 @@ object ScanCardProcessor { private suspend inline fun showDisclaimerIfNeed( scanResponse: ScanResponse, + crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit, crossinline nextHandler: suspend (ScanResponse) -> Unit, ) { val disclaimerType = DisclaimerType.get(scanResponse) @@ -137,11 +139,18 @@ object ScanCardProcessor { } else scope.launch(Dispatchers.Main) { delay(DELAY_SDK_DIALOG_CLOSE) store.dispatchOnMain( - DisclaimerAction.Show { - scope.launch(Dispatchers.Main) { - nextHandler(scanResponse) - } - }, + DisclaimerAction.Show( + onAcceptCallback = { + scope.launch(Dispatchers.Main) { + nextHandler(scanResponse) + } + }, + onDismissCallback = { + scope.launch(Dispatchers.Main) { + onProgressStateChange(false) + } + }, + ), ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt index 512e986cbf..0c0f909c80 100644 --- a/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt +++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt @@ -33,9 +33,6 @@ internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator { when (walletsData.findStatus()) { TotalFiatBalanceStatus.Loading -> TotalFiatBalance.Loading - TotalFiatBalanceStatus.Refreshing -> TotalFiatBalance.Refreshing( - amount = prevAmount ?: BigDecimal.ZERO, - ) TotalFiatBalanceStatus.Error -> TotalFiatBalance.Error(calculateAmount()) TotalFiatBalanceStatus.Loaded -> TotalFiatBalance.Loaded(calculateAmount()) } @@ -54,7 +51,6 @@ internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator { private fun Sequence.mapToStatus(): Sequence { return this.map { walletData -> when (walletData.status) { - is WalletDataModel.Refreshing -> TotalFiatBalanceStatus.Refreshing is WalletDataModel.VerifiedOnline, is WalletDataModel.SameCurrencyTransactionInProgress, is WalletDataModel.TransactionInProgress, @@ -84,18 +80,10 @@ internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator { ): TotalFiatBalanceStatus { return when (prevStatus) { TotalFiatBalanceStatus.Loading -> prevStatus - TotalFiatBalanceStatus.Refreshing -> when (newStatus) { - TotalFiatBalanceStatus.Loading -> prevStatus - TotalFiatBalanceStatus.Refreshing, - TotalFiatBalanceStatus.Error, - TotalFiatBalanceStatus.Loaded, - -> newStatus - } TotalFiatBalanceStatus.Loaded, TotalFiatBalanceStatus.Error, -> when (newStatus) { TotalFiatBalanceStatus.Loading, - TotalFiatBalanceStatus.Refreshing, TotalFiatBalanceStatus.Error, -> newStatus TotalFiatBalanceStatus.Loaded -> prevStatus @@ -105,7 +93,6 @@ internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator { private enum class TotalFiatBalanceStatus { Loading, - Refreshing, Error, Loaded, } diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt index ae9a72152a..28016d704f 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt @@ -10,6 +10,9 @@ import com.tangem.operations.wallet.CreateWalletTask import com.tangem.operations.wallet.PurgeWalletCommand class CreateFirstTwinWalletTask(private val firstCardId: String) : CardSessionRunnable { + + override val allowsRequestAccessCodeFromRepository: Boolean = false + override fun run( session: CardSession, callback: (result: CompletionResult) -> Unit, diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt index 57b66d7a62..628c041112 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt @@ -20,6 +20,8 @@ class CreateSecondTwinWalletTask( private val creatingWalletMessage: Message, ) : CardSessionRunnable { + override val allowsRequestAccessCodeFromRepository: Boolean = false + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { val card = session.environment.card val publicKey = card?.wallets?.firstOrNull()?.publicKey diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 73b9e2f40a..d4e00cb3fe 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -13,6 +13,8 @@ class FinalizeTwinTask( private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair, ) : CardSessionRunnable { + override val allowsRequestAccessCodeFromRepository: Boolean = false + override fun run( session: CardSession, callback: (result: CompletionResult) -> Unit, diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt index 8310047019..b910fcb589 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -20,11 +20,9 @@ class TwinCardsManager( assetReader: AssetReader, ) { private val firstCardId: String = card.cardId - private var secondCardId: String? = null private var currentCardPublicKey: String? = null - var secondCardPublicKey: String? = null - private set + private var secondCardPublicKey: String? = null private val issuerKeyPair: KeyPair = getIssuerKeys(assetReader, card.issuer.publicKey.toHexString()) @@ -57,7 +55,6 @@ class TwinCardsManager( when (response) { is CompletionResult.Success -> { secondCardPublicKey = response.data.wallet.publicKey.toHexString() - secondCardId = response.data.cardId } is CompletionResult.Failure -> {} } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt index 62ea1a37a4..dfd62c05f5 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt @@ -14,24 +14,23 @@ interface UserWalletsListManager { val hasSavedUserWallets: Boolean suspend fun unlockWithBiometry(): CompletionResult - suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult fun lock() - suspend fun selectWallet(walletId: UserWalletId): CompletionResult + suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult /** - * Save user's wallet - * @param userWallet User's wallet to save + * Save user wallet + * @param userWallet [UserWallet] to save * @param canOverride If false, then terminate with [UserWalletListError.WalletAlreadySaved] when user tries to save an * already saved card - * @return [CompletionResult] operation result + * @return [CompletionResult] of operation * */ suspend fun save(userWallet: UserWallet, canOverride: Boolean = false): CompletionResult - suspend fun delete(walletIds: List): CompletionResult + suspend fun delete(userWalletIds: List): CompletionResult suspend fun clear(): CompletionResult - suspend fun get(walletId: UserWalletId): CompletionResult + suspend fun get(userWalletId: UserWalletId): CompletionResult companion object } \ No newline at end of file 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 248719dd20..f7781ca770 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 @@ -28,21 +28,19 @@ internal class BiometricUserWalletsListManager( override val userWallets: Flow> get() = state - .mapLatest { it.wallets } + .mapLatest { it.userWallets } .distinctUntilChanged() override val selectedUserWallet: Flow get() = state .mapLatest { state -> - state.wallets.find { - it.walletId == state.selectedWalletId - } + findSelectedUserWallet(state.userWallets) } .filterNotNull() .distinctUntilChanged() override val selectedUserWalletSync: UserWallet? - get() = findSelectedWallet() + get() = findSelectedUserWallet() override val isLocked: Flow get() = state @@ -53,44 +51,13 @@ internal class BiometricUserWalletsListManager( get() = state.value.isLocked override val hasSavedUserWallets: Boolean - get() = selectedUserWalletRepository.get() != null + get() = publicInformationRepository.isNotEmpty() override suspend fun unlockWithBiometry(): CompletionResult { return unlockWithBiometryInternal() .map { selectedUserWalletSync } } - override suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult { - state.update { prevState -> - // If the previous state contains a saved user wallet with the same ID, it is also saved - userWallet.isSaved = prevState.wallets.any { - it.walletId == userWallet.walletId && it.isSaved - } - - val newEncryptionKeys = prevState.encryptionKeys - .plus(UserWalletEncryptionKey(userWallet)) - .distinctBy { it.walletId } - val newUserWallets = prevState.wallets - .plus(userWallet) - .distinctBy { it.walletId } - - prevState.copy( - encryptionKeys = newEncryptionKeys, - wallets = newUserWallets, - selectedWalletId = userWallet.walletId, - ) - } - - return loadModels() - .map { - state.update { prevState -> - prevState.copy( - isLocked = prevState.wallets.any { it.isLocked }, - ) - } - } - } - override fun lock() { state.update { prevState -> prevState.copy( @@ -100,85 +67,62 @@ internal class BiometricUserWalletsListManager( } } - override suspend fun selectWallet(walletId: UserWalletId): CompletionResult = catching { - if (state.value.selectedWalletId == walletId) { - return@catching findSelectedWallet()!! + override suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult = catching { + if (state.value.selectedUserWalletId == userWalletId) { + return@catching findSelectedUserWallet()!! } - if (!state.value.isLocked) { - selectedUserWalletRepository.set(walletId) + selectedUserWalletRepository.set(userWalletId) - state.update { prevState -> - prevState.copy( - selectedWalletId = walletId, - ) - } + state.update { prevState -> + prevState.copy( + selectedUserWalletId = userWalletId, + ) } - findSelectedWallet()!! + findSelectedUserWallet()!! } - override suspend fun save( - userWallet: UserWallet, - canOverride: Boolean, - ): CompletionResult = withUnlock { - val isWalletSaved = state.value.wallets - .filter { it.isSaved } - .any { - // Workaround, check [UserWallet.isTwinnedWith] - it.cardsInWallet.contains(userWallet.cardId) || it.isTwinnedWith(userWallet) - } - - if (isWalletSaved && !canOverride) { - CompletionResult.Failure(UserWalletListError.WalletAlreadySaved) + override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult { + return if (canOverride) { + saveInternal(userWallet) } else { - val newEncryptionKeys = state.value.encryptionKeys - .plus(UserWalletEncryptionKey(userWallet)) - .distinctBy { it.walletId } + val isWalletSaved = state.value.userWallets + .any { + // Workaround, check [UserWallet.isTwinnedWith] + it.cardsInWallet.contains(userWallet.cardId) || it.isTwinnedWith(userWallet) + } - keysRepository.store(newEncryptionKeys) - .doOnSuccess { - state.update { prevState -> - prevState.copy( - encryptionKeys = newEncryptionKeys, - ) - } - } - .flatMap { publicInformationRepository.save(userWallet) } - .flatMap { sensitiveInformationRepository.save(userWallet) } - .flatMap { loadModels() } - .doOnSuccess { - userWallet.isSaved = true - } + if (isWalletSaved) { + CompletionResult.Failure(UserWalletListError.WalletAlreadySaved) + } else { + saveInternal(userWallet) + } } } - override suspend fun delete(walletIds: List): CompletionResult { - val walletIdsToRemove = state.value.wallets - .map { it.walletId } - .filter { it in walletIds } - val remainingEncryptionKeys = state.value.encryptionKeys - .filter { it.walletId !in walletIdsToRemove } + override suspend fun delete(userWalletIds: List): CompletionResult { + if (userWalletIds.isEmpty()) { + return CompletionResult.Success(Unit) + } - changeSelectedWalletIfNeeded(walletIdsToRemove) + changeSelectedUserWalletIdIfNeeded(userWalletIds) - return sensitiveInformationRepository.delete(walletIdsToRemove) - .flatMap { publicInformationRepository.delete(walletIdsToRemove) } - .flatMap { keysRepository.store(remainingEncryptionKeys) } + return sensitiveInformationRepository.delete(userWalletIds) + .flatMap { publicInformationRepository.delete(userWalletIds) } + .flatMap { keysRepository.delete(userWalletIds) } .map { state.update { prevState -> prevState.copy( - encryptionKeys = remainingEncryptionKeys, - wallets = prevState.wallets.filter { it.walletId !in walletIdsToRemove }, + encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in userWalletIds }, + userWallets = prevState.userWallets.filter { it.walletId !in userWalletIds }, ) } } } override suspend fun clear(): CompletionResult { - return sensitiveInformationRepository.delete( - walletIds = state.value.wallets.map { it.walletId }, - ) + return sensitiveInformationRepository.clear() .flatMap { publicInformationRepository.clear() } .flatMap { keysRepository.clear() } .map { @@ -187,17 +131,37 @@ internal class BiometricUserWalletsListManager( } } - override suspend fun get(walletId: UserWalletId): CompletionResult { + override suspend fun get(userWalletId: UserWalletId): CompletionResult { return catching { - state.value.wallets.first { it.walletId == walletId } + state.value.userWallets.first { it.walletId == userWalletId } } } - private suspend inline fun withUnlock( - block: () -> CompletionResult, - ): CompletionResult { - return (if (state.value.isLocked) unlockWithBiometryInternal() else CompletionResult.Success(Unit)) - .flatMap { block() } + private suspend fun saveInternal(userWallet: UserWallet): CompletionResult { + val newEncryptionKeys = state.value.encryptionKeys + .plus(UserWalletEncryptionKey(userWallet)) + .distinctBy { it.walletId } + + return keysRepository.store(newEncryptionKeys) + .doOnSuccess { + state.update { prevState -> + prevState.copy( + encryptionKeys = newEncryptionKeys, + selectedUserWalletId = userWallet.walletId, + ) + } + } + .flatMap { publicInformationRepository.save(userWallet) } + .flatMap { sensitiveInformationRepository.save(userWallet) } + .map { selectedUserWalletRepository.set(userWallet.walletId) } + .flatMap { loadModels() } + .doOnSuccess { + state.update { prevState -> + prevState.copy( + isLocked = prevState.userWallets.any { it.isLocked }, + ) + } + } } private suspend fun unlockWithBiometryInternal(): CompletionResult { @@ -223,11 +187,11 @@ internal class BiometricUserWalletsListManager( return getSavedUserWallets() .map { userWallets -> if (userWallets.isNotEmpty()) state.update { prevState -> - val wallets = (userWallets + prevState.wallets).distinctBy { it.walletId } + val wallets = (userWallets + prevState.userWallets).distinctBy { it.walletId } prevState.copy( - wallets = wallets, - selectedWalletId = findOrSetSelectedWallet(prevState.selectedWalletId, wallets), + userWallets = wallets, + selectedUserWalletId = findOrSetSelectedUserWalletId(prevState.selectedUserWalletId, wallets), ) } } @@ -247,56 +211,55 @@ internal class BiometricUserWalletsListManager( } } - private fun findOrSetSelectedWallet( + private fun findOrSetSelectedUserWalletId( prevSelectedWalletId: UserWalletId?, userWallets: List, ): UserWalletId? { - return prevSelectedWalletId - ?: (selectedUserWalletRepository.get() - ?: (userWallets.firstOrNull()?.walletId - ?.also { selectedUserWalletRepository.set(it) })) + val findUnlockedAndSet = { + userWallets.firstOrNull { !it.isLocked } + ?.walletId + ?.also { selectedUserWalletRepository.set(it) } + } + + return prevSelectedWalletId ?: (selectedUserWalletRepository.get() ?: findUnlockedAndSet()) } - private fun changeSelectedWalletIfNeeded( - walletsIdsToRemove: List, - ) { - val remainingWallets = state.value.wallets.filter { + private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List) { + val remainingWallets = state.value.userWallets.filter { it.walletId !in walletsIdsToRemove } - val selectedWallet = findSelectedWallet() + val selectedWallet = findSelectedUserWallet() when { remainingWallets.isEmpty() -> { state.update { prevState -> prevState.copy( - selectedWalletId = null, + selectedUserWalletId = null, ) } selectedUserWalletRepository.set(null) } !remainingWallets.contains(selectedWallet) -> { - val newSelectedWallet = remainingWallets.first() + val newSelectedWallet = remainingWallets.firstOrNull { !it.isLocked } state.update { prevState -> prevState.copy( - selectedWalletId = newSelectedWallet.walletId, + selectedUserWalletId = newSelectedWallet?.walletId, ) } - selectedUserWalletRepository.set(newSelectedWallet.walletId) + selectedUserWalletRepository.set(newSelectedWallet?.walletId) } } } - private fun findSelectedWallet(): UserWallet? { - return with(state.value) { - wallets.find { - it.walletId == selectedWalletId - } + private fun findSelectedUserWallet(userWallets: List = state.value.userWallets): UserWallet? { + return userWallets.find { + it.walletId == state.value.selectedUserWalletId } } private data class State( val encryptionKeys: List = emptyList(), - val wallets: List = emptyList(), - val selectedWalletId: UserWalletId? = null, + val userWallets: List = emptyList(), + val selectedUserWalletId: UserWalletId? = null, val isLocked: Boolean = true, ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt index a3c3a3e929..e616c48758 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt @@ -26,15 +26,11 @@ class DummyUserWalletsListManager : UserWalletsListManager { return CompletionResult.Success(null) } - override suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult { - return CompletionResult.Success(Unit) - } - override fun lock() { /* no-op */ } - override suspend fun selectWallet(walletId: UserWalletId): CompletionResult { + override suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult { return catching { error("Not implemented") } @@ -44,7 +40,7 @@ class DummyUserWalletsListManager : UserWalletsListManager { return CompletionResult.Success(Unit) } - override suspend fun delete(walletIds: List): CompletionResult { + override suspend fun delete(userWalletIds: List): CompletionResult { return CompletionResult.Success(Unit) } @@ -52,7 +48,7 @@ class DummyUserWalletsListManager : UserWalletsListManager { return CompletionResult.Success(Unit) } - override suspend fun get(walletId: UserWalletId): CompletionResult { + override suspend fun get(userWalletId: UserWalletId): CompletionResult { return catching { error("Not implemented") } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt index b2308343ff..d383e11e3f 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt @@ -17,4 +17,5 @@ internal data class UserWalletPublicInformation( val artworkUrl: String, val cardsInWallet: Set, val scanResponse: ScanResponse, + val isMultiCurrency: Boolean, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt index 974879ca31..f10e0beeb5 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt @@ -1,10 +1,35 @@ package com.tangem.tap.domain.userWalletList.repository import com.tangem.common.CompletionResult +import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey internal interface UserWalletsKeysRepository { + /** + * Obtaining the encryption keys of all user wallets from the biometric vault. Biometric authentication required + * If that operation runs more than biometric cipher key expiration time then the user will not receive all + * encryption keys + * @return [CompletionResult] of operation with stored [UserWalletEncryptionKey] list + * */ suspend fun getAll(): CompletionResult> + + /** + * Store the encryption keys for user wallets. Biometric authentication not required + * @param encryptionKeys List of encryption keys for user wallets + * @return [CompletionResult] of operation + * */ suspend fun store(encryptionKeys: List): CompletionResult + + /** + * Delete encryption keys for user wallets. Biometric authentication not required + * @param userWalletsIds List of [UserWalletId] whose encryption keys will be deleted + * @return [CompletionResult] of operation + * */ + suspend fun delete(userWalletsIds: List): CompletionResult + + /** + * Clear all encryption keys for user wallets. Biometric authentication not required + * @return [CompletionResult] of operation + * */ suspend fun clear(): CompletionResult } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsPublicInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsPublicInformationRepository.kt index 8765a6b804..f0387d3f0e 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsPublicInformationRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsPublicInformationRepository.kt @@ -12,4 +12,6 @@ internal interface UserWalletsPublicInformationRepository { suspend fun delete(walletIds: List): CompletionResult suspend fun clear(): CompletionResult + + fun isNotEmpty(): Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsSensitiveInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsSensitiveInformationRepository.kt index dcc5677814..7e03055150 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsSensitiveInformationRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsSensitiveInformationRepository.kt @@ -12,5 +12,6 @@ internal interface UserWalletsSensitiveInformationRepository { encryptionKeys: List, ): CompletionResult> - suspend fun delete(walletIds: List): CompletionResult + suspend fun delete(userWalletsIds: List): CompletionResult + suspend fun clear(): CompletionResult } \ 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 cd39605d96..5e57d10474 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 @@ -6,62 +6,183 @@ import com.squareup.moshi.Types import com.tangem.common.CompletionResult import com.tangem.common.biometric.BiometricManager import com.tangem.common.biometric.BiometricStorage +import com.tangem.common.core.TangemSdkError +import com.tangem.common.doOnFailure +import com.tangem.common.fold import com.tangem.common.map import com.tangem.common.mapFailure import com.tangem.common.services.secure.SecureStorage +import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.domain.userWalletList.UserWalletListError import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext internal class BiometricUserWalletsKeysRepository( moshi: Moshi, - secureStorage: SecureStorage, biometricManager: BiometricManager, + private val secureStorage: SecureStorage, ) : UserWalletsKeysRepository { private val biometricStorage = BiometricStorage( biometricManager = biometricManager, secureStorage = secureStorage, ) - private val walletsKeysAdapter: JsonAdapter> = moshi.adapter( - Types.newParameterizedType(List::class.java, UserWalletEncryptionKey::class.java), + private val encryptionKeyAdapter: JsonAdapter = moshi.adapter( + UserWalletEncryptionKey::class.java, + ) + private val userWalletsIdsListAdapter: JsonAdapter> = moshi.adapter( + Types.newParameterizedType(List::class.java, UserWalletId::class.java), ) override suspend fun getAll(): CompletionResult> { - return biometricStorage.get(key = StorageKey.WalletEncryptionKeys.name) - .map { encryptionKeys -> - encryptionKeys.decodeToKeys() - } - .mapFailure { error -> - UserWalletListError.ReceiveEncryptionKeysError(error.cause ?: error) - } + return withContext(Dispatchers.IO) { + getAllInternal() + .mapFailure { error -> + UserWalletListError.ReceiveEncryptionKeysError(error.cause ?: error) + } + } } override suspend fun store(encryptionKeys: List): CompletionResult { - return biometricStorage.store( - key = StorageKey.WalletEncryptionKeys.name, - data = encryptionKeys.encode(), - ) - .mapFailure { error -> - UserWalletListError.SaveEncryptionKeysError(error.cause ?: error) + return withContext(Dispatchers.IO) { + encryptionKeys.map { storeEncryptionKey(it) } + .fold() + .mapFailure { error -> + UserWalletListError.SaveEncryptionKeysError(error.cause ?: error) + } + } + } + + override suspend fun delete(userWalletsIds: List): CompletionResult { + return withContext(Dispatchers.IO) { + userWalletsIds.map { userWalletId -> + deleteEncryptionKey(userWalletId) } + .fold() + .map { deleteUserWalletsIds(userWalletsIds) } + } } override suspend fun clear(): CompletionResult { - return biometricStorage.delete(key = StorageKey.WalletEncryptionKeys.name) + return withContext(Dispatchers.IO) { + getUserWalletsIds() + .map { userWalletId -> + deleteEncryptionKey(userWalletId) + } + .fold() + .map { + clearUserWalletsIds() + } + } } - private fun List.encode(): ByteArray { - return this.let(walletsKeysAdapter::toJson) - .encodeToByteArray(throwOnInvalidSequence = true) + private suspend fun getAllInternal(): CompletionResult> { + return getUserWalletsIds() + .map { userWalletId -> + // This is possible because the Card SDK cipher key has an expiration time + // If this operation runs more than that expiration time, the user will not receive all encryption keys + getEncryptionKey(userWalletId) + .doOnFailure { error -> + // If the user cancels biometric authentication, cancel the request for all keys + if (error is TangemSdkError.BiometricsAuthenticationFailed) { + return CompletionResult.Failure(error) + } + } + } + .fold(listOf()) { acc, data -> + if (data != null) acc + data else acc + } } - private fun ByteArray?.decodeToKeys(): List { - return this?.decodeToString(throwOnInvalidSequence = true) - ?.let(walletsKeysAdapter::fromJson) - .orEmpty() + private suspend fun getEncryptionKey(userWalletId: UserWalletId): CompletionResult { + return biometricStorage.get(StorageKey.WalletEncryptionKey(userWalletId).name) + .map { it.decodeToKey() } } - private enum class StorageKey { - WalletEncryptionKeys + private suspend fun storeEncryptionKey(encryptionKey: UserWalletEncryptionKey): CompletionResult { + return biometricStorage.store( + key = StorageKey.WalletEncryptionKey(encryptionKey.walletId).name, + data = encryptionKey.encode(), + ) + .map { storeUserWalletId(encryptionKey.walletId) } + } + + private suspend fun deleteEncryptionKey(userWalletId: UserWalletId): CompletionResult { + return biometricStorage.delete(StorageKey.WalletEncryptionKey(userWalletId).name) + } + + private suspend fun getUserWalletsIds(): List { + return withContext(Dispatchers.IO) { + secureStorage.get(StorageKey.UserWalletIds.name) + .decodeToUserWalletsIds() + } + } + + private suspend fun storeUserWalletId(userWalletId: UserWalletId) { + val userWalletIds = (getUserWalletsIds() + userWalletId).distinct() + + withContext(Dispatchers.IO) { + secureStorage.store(userWalletIds.encode(), StorageKey.UserWalletIds.name) + } + } + + private suspend fun deleteUserWalletsIds(userWalletsIds: List) { + val remainingIds = (getUserWalletsIds() - userWalletsIds.toSet()) + + withContext(Dispatchers.IO) { + secureStorage.store(remainingIds.encode(), StorageKey.UserWalletIds.name) + } + } + + private suspend fun clearUserWalletsIds() { + withContext(Dispatchers.IO) { + secureStorage.delete(StorageKey.UserWalletIds.name) + } + } + + private suspend fun UserWalletEncryptionKey.encode(): ByteArray { + return withContext(Dispatchers.Default) { + this@encode + .let(encryptionKeyAdapter::toJson) + .encodeToByteArray(throwOnInvalidSequence = true) + } + } + + private suspend fun ByteArray?.decodeToKey(): UserWalletEncryptionKey? { + return withContext(Dispatchers.Default) { + this@decodeToKey + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(encryptionKeyAdapter::fromJson) + } + } + + private suspend fun List.encode(): ByteArray { + return withContext(Dispatchers.Default) { + this@encode + .let(userWalletsIdsListAdapter::toJson) + .encodeToByteArray(throwOnInvalidSequence = true) + } + } + + private suspend fun ByteArray?.decodeToUserWalletsIds(): List { + return withContext(Dispatchers.Default) { + this@decodeToUserWalletsIds + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(userWalletsIdsListAdapter::fromJson) + .orEmpty() + } + } + + private sealed interface StorageKey { + val name: String + + class WalletEncryptionKey(userWalletId: UserWalletId) : StorageKey { + override val name: String = "user_wallet_encryption_key_${userWalletId.stringValue}" + } + + object UserWalletIds : StorageKey { + override val name: String = "user_wallets_ids_with_saved_keys" + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt index 46d0fc073b..46a016f8c0 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt @@ -25,19 +25,21 @@ internal class DefaultUserWalletsPublicInformationRepository( ) override suspend fun save(userWallet: UserWallet): CompletionResult { - return getAll() - .flatMap { savedInformation -> - val infoToSave = withContext(Dispatchers.Default) { - savedInformation.toMutableList() - .apply { - replaceByOrAdd(userWallet.publicInformation) { - userWallet.walletId == it.walletId + return withContext(Dispatchers.IO) { + getAll() + .flatMap { savedInformation -> + val infoToSave = withContext(Dispatchers.Default) { + savedInformation.toMutableList() + .apply { + replaceByOrAdd(userWallet.publicInformation) { + userWallet.walletId == it.walletId + } } - } - } + } - save(infoToSave) - } + save(infoToSave) + } + } } override suspend fun getAll(): CompletionResult> = catching { @@ -50,20 +52,28 @@ internal class DefaultUserWalletsPublicInformationRepository( } override suspend fun delete(walletIds: List): CompletionResult { - return getAll() - .flatMap { publicInformation -> - val infoToRemove = publicInformation - .filter { it.walletId in walletIds } - .toSet() + return withContext(Dispatchers.IO) { + getAll() + .flatMap { publicInformation -> + val infoToRemove = publicInformation + .filter { it.walletId in walletIds } + .toSet() - save( - publicInformation = publicInformation - infoToRemove, - ) - } + save( + publicInformation = publicInformation - infoToRemove, + ) + } + } } override suspend fun clear(): CompletionResult = catching { - secureStorage.delete(StorageKey.UserWalletPublicInformation.name) + withContext(Dispatchers.IO) { + secureStorage.delete(StorageKey.UserWalletPublicInformation.name) + } + } + + override fun isNotEmpty(): Boolean { + return secureStorage.get(StorageKey.UserWalletPublicInformation.name)?.isNotEmpty() == true } @JvmName("saveWithPublicInformation") diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt index c338d17c67..5918312b6e 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt @@ -3,12 +3,14 @@ package com.tangem.tap.domain.userWalletList.repository.implementation import android.security.keystore.KeyProperties import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi +import com.squareup.moshi.Types import com.tangem.common.CompletionResult import com.tangem.common.catching import com.tangem.common.mapFailure import com.tangem.common.services.secure.SecureStorage import com.tangem.domain.common.util.UserWalletId import com.tangem.domain.common.util.encryptionKey +import com.tangem.tap.common.extensions.filterNotNull import com.tangem.tap.domain.model.UserWallet import com.tangem.tap.domain.userWalletList.UserWalletListError import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey @@ -28,29 +30,23 @@ internal class DefaultUserWalletsSensitiveInformationRepository( private val sensitiveInformationAdapter: JsonAdapter = moshi.adapter( UserWalletSensitiveInformation::class.java, ) + private val encryptedSensitiveInformationMapAdapter: JsonAdapter> = moshi.adapter( + Types.newParameterizedType(Map::class.java, String::class.java, ByteArray::class.java), + ) + private val cipher: Cipher by lazy { Cipher.getInstance("$algorithm/$blockMode/$encryptionPadding") } override suspend fun save(userWallet: UserWallet): CompletionResult { return catching { - withContext(Dispatchers.Default) { - userWallet.sensitiveInformation - .let(sensitiveInformationAdapter::toJson) - .encodeToByteArray(throwOnInvalidSequence = true) - .encryptAndStoreIv( - walletId = userWallet.walletId, - encryptionKey = userWallet.scanResponse.card.encryptionKey, - ) - .let { encryptedInformation -> - withContext(Dispatchers.IO) { - secureStorage.store( - data = encryptedInformation, - account = StorageKey.SensitiveInformation(userWallet.walletId).name, - ) - } - } - } + val encryptedSensitiveInformation = userWallet.sensitiveInformation + .encode() + .encryptAndStoreIv(userWallet.walletId.stringValue, userWallet.scanResponse.card.encryptionKey) + + getAllEncrypted().toMutableMap() + .apply { set(userWallet.walletId.stringValue, encryptedSensitiveInformation) } + .let { saveInternal(it) } } .mapFailure { error -> UserWalletListError.SaveSensitiveInformationError(error.cause ?: error) @@ -60,77 +56,130 @@ internal class DefaultUserWalletsSensitiveInformationRepository( override suspend fun getAll( encryptionKeys: List, ): CompletionResult> { + if (encryptionKeys.isEmpty()) { + return CompletionResult.Success(emptyMap()) + } + return catching { - if (encryptionKeys.isEmpty()) { - return@catching emptyMap() - } + val encryptedSensitiveInformation = getAllEncrypted() - val keyToEncryptedInformation = withContext(Dispatchers.IO) { - encryptionKeys.associateWith { encryptionKey -> - secureStorage.get(StorageKey.SensitiveInformation(encryptionKey.walletId).name) + encryptionKeys + .associateBy { it.walletId } + .mapValues { (userWalletId, encryptionKey) -> + encryptedSensitiveInformation[userWalletId.stringValue] + ?.getIvAndDecrypt(userWalletId.stringValue, encryptionKey.encryptionKey) + .decodeToSensitiveInformation() } - } - - withContext(Dispatchers.Default) { - val keyToInformation = - mutableMapOf() - - keyToEncryptedInformation.forEach { (key, encryptedInformation) -> - val information = encryptedInformation - ?.getIvAndDecrypt( - walletId = key.walletId, - encryptionKey = key.encryptionKey, - ) - ?.decodeToString(throwOnInvalidSequence = true) - ?.let(sensitiveInformationAdapter::fromJson) - - if (information != null) { - keyToInformation[key.walletId] = information - } - } - - keyToInformation - } + .filterNotNull() } .mapFailure { error -> UserWalletListError.ReceiveSensitiveInformationError(error.cause ?: error) } } - override suspend fun delete(walletIds: List): CompletionResult = catching { - walletIds - .forEach { walletId -> - secureStorage.delete(StorageKey.SensitiveInformation(walletId).name) - } + override suspend fun delete(userWalletsIds: List): CompletionResult { + return catching { deleteInternal(userWalletsIds) } } - private fun ByteArray.encryptAndStoreIv(walletId: UserWalletId, encryptionKey: ByteArray): ByteArray { - val secretKey = SecretKeySpec(encryptionKey, algorithm) - cipher.init(Cipher.ENCRYPT_MODE, secretKey) - val encryptedData = cipher.doFinal(this) - secureStorage.store(data = cipher.iv, account = StorageKey.SensitiveInformationIv(walletId).name) - return encryptedData + override suspend fun clear(): CompletionResult { + return catching { clearInternal() } } - private fun ByteArray.getIvAndDecrypt(walletId: UserWalletId, encryptionKey: ByteArray): ByteArray? { - val iv = secureStorage.get(StorageKey.SensitiveInformationIv(walletId).name) - ?: error("IV not found") - val ivParam = IvParameterSpec(iv) - val secretKeySpec = SecretKeySpec(encryptionKey, algorithm) - return cipher - .also { it.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParam) } - .doFinal(this) + private suspend fun getAllEncrypted(): Map { + return withContext(Dispatchers.IO) { + secureStorage.get(StorageKey.UserWalletsSensitiveInformation.name) + .decodeToEncryptedSensitiveInformation() + } + } + + private suspend fun saveInternal(sensitiveInformation: Map) { + return withContext(Dispatchers.IO) { + secureStorage.store( + account = StorageKey.UserWalletsSensitiveInformation.name, + data = sensitiveInformation.encode(), + ) + } + } + + private suspend fun deleteInternal(userWalletsIds: List) { + return saveInternal( + sensitiveInformation = getAllEncrypted() - userWalletsIds.map { it.stringValue }.toSet(), + ) + } + + private suspend fun clearInternal() { + withContext(Dispatchers.IO) { + secureStorage.delete(StorageKey.UserWalletsSensitiveInformation.name) + } + } + + private suspend fun ByteArray?.decodeToEncryptedSensitiveInformation(): Map { + return withContext(Dispatchers.Default) { + this@decodeToEncryptedSensitiveInformation + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(encryptedSensitiveInformationMapAdapter::fromJson) + .orEmpty() + } + } + + private suspend fun ByteArray?.decodeToSensitiveInformation(): UserWalletSensitiveInformation? { + return withContext(Dispatchers.Default) { + this@decodeToSensitiveInformation + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(sensitiveInformationAdapter::fromJson) + } + } + + private suspend fun UserWalletSensitiveInformation.encode(): ByteArray { + return withContext(Dispatchers.Default) { + this@encode + .let(sensitiveInformationAdapter::toJson) + .encodeToByteArray(throwOnInvalidSequence = true) + } + } + + private suspend fun Map.encode(): ByteArray { + return withContext(Dispatchers.Default) { + this@encode + .let(encryptedSensitiveInformationMapAdapter::toJson) + .encodeToByteArray(throwOnInvalidSequence = true) + } + } + + private suspend fun ByteArray.encryptAndStoreIv(userWalletId: String, encryptionKey: ByteArray): ByteArray { + return withContext(Dispatchers.Default) { + val secretKey = SecretKeySpec(encryptionKey, algorithm) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + val encryptedData = cipher.doFinal(this@encryptAndStoreIv) + secureStorage.store(data = cipher.iv, account = StorageKey.SensitiveInformationIv(userWalletId).name) + encryptedData + } + } + + private suspend fun ByteArray.getIvAndDecrypt( + userWalletId: String, + encryptionKey: ByteArray, + ): ByteArray? { + return withContext(Dispatchers.Default) { + val iv = secureStorage.get(StorageKey.SensitiveInformationIv(userWalletId).name) + ?: error("IV not found") + val ivParam = IvParameterSpec(iv) + val secretKeySpec = SecretKeySpec(encryptionKey, algorithm) + cipher + .also { it.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParam) } + .doFinal(this@getIvAndDecrypt) + } } private sealed interface StorageKey { val name: String - class SensitiveInformation(walletId: UserWalletId) : StorageKey { - override val name: String = "user_wallet_sensitive_information_${walletId.stringValue}" + object UserWalletsSensitiveInformation : StorageKey { + override val name: String = "user_wallets_sensitive_information" } - class SensitiveInformationIv(walletId: UserWalletId) : StorageKey { - override val name: String = "user_wallet_sensitive_information_iv_${walletId.stringValue}" + class SensitiveInformationIv(userWalletId: String) : StorageKey { + override val name: String = "user_wallet_sensitive_information_iv_${userWalletId}" } } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt index 810ba463f6..7d8170895c 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt @@ -14,6 +14,7 @@ internal val UserWallet.publicInformation: UserWalletPublicInformation walletId = walletId, artworkUrl = artworkUrl, cardsInWallet = cardsInWallet, + isMultiCurrency = isMultiCurrency, scanResponse = scanResponse.copy( card = scanResponse.card.copy( wallets = emptyList(), @@ -28,6 +29,7 @@ internal fun UserWalletPublicInformation.toUserWallet(): UserWallet { artworkUrl = artworkUrl, cardsInWallet = cardsInWallet, scanResponse = scanResponse, + isMultiCurrency = isMultiCurrency, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletCurrencies/WalletCurrenciesManager.kt b/app/src/main/java/com/tangem/tap/domain/walletCurrencies/WalletCurrenciesManager.kt index f1115ee3ef..fb77a29584 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletCurrencies/WalletCurrenciesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletCurrencies/WalletCurrenciesManager.kt @@ -2,13 +2,12 @@ package com.tangem.tap.domain.walletCurrencies import com.tangem.common.CompletionResult import com.tangem.tap.domain.model.UserWallet -import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.models.Currency interface WalletCurrenciesManager { suspend fun update( userWallet: UserWallet, - blockchainNetwork: BlockchainNetwork, + currency: Currency, ): CompletionResult suspend fun addCurrencies( diff --git a/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt b/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt index 837ff8452c..5fd2e25234 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt @@ -1,25 +1,25 @@ package com.tangem.tap.domain.walletCurrencies.implementation +import com.tangem.blockchain.common.DerivationStyle import com.tangem.common.CompletionResult -import com.tangem.common.catching import com.tangem.common.flatMap +import com.tangem.common.fold +import com.tangem.common.map import com.tangem.domain.common.CardDTO import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.domain.model.UserWallet import com.tangem.tap.domain.model.builders.WalletStoreBuilder import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager -import com.tangem.tap.domain.walletStores.implementation.utils.fold import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.models.getTokens -import com.tangem.tap.features.wallet.models.toCurrencies +import com.tangem.tap.features.wallet.models.toBlockchainNetworks import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext internal class DefaultWalletCurrenciesManager( @@ -31,18 +31,18 @@ internal class DefaultWalletCurrenciesManager( ) : WalletCurrenciesManager { override suspend fun update( userWallet: UserWallet, - blockchainNetwork: BlockchainNetwork, - ): CompletionResult { - val walletStore = walletStoresRepository.get(userWallet.walletId).first() + currency: Currency, + ): CompletionResult = withContext(Dispatchers.Default) { + val walletStore = walletStoresRepository.getSync(userWallet.walletId) .find { - it.blockchainNetwork.blockchain == blockchainNetwork.blockchain - && it.blockchainNetwork.derivationPath == blockchainNetwork.derivationPath + it.blockchain == currency.blockchain + && it.derivationPath?.rawPath == currency.derivationPath } - return if (walletStore != null) { - walletAmountsRepository.update( - userWallet = userWallet, + if (walletStore != null) { + walletAmountsRepository.updateAmountsForWalletStore( walletStore = walletStore, + userWallet = userWallet, fiatCurrency = appCurrencyProvider(), ) } else CompletionResult.Success(Unit) @@ -52,25 +52,42 @@ internal class DefaultWalletCurrenciesManager( userWallet: UserWallet, currenciesToAdd: List, ): CompletionResult = withContext(Dispatchers.Default) { - var newBlockchainNetworks = listOf() - catching { - val card = userWallet.scanResponse.card - val savedCurrencies = withContext(Dispatchers.IO) { - userTokensRepository.getUserTokens(card) - } - newBlockchainNetworks = (savedCurrencies + currenciesToAdd) - .toBlockchainNetworks(userWallet.scanResponse.card) - val newCurrencies = newBlockchainNetworks.toCurrencies() + val card = userWallet.scanResponse.card + val updatedBlockchainNetworks = currenciesToAdd + .addMissingBlockchains(card) + .toBlockchainNetworks() + val newCurrencies = (getSavedCurrencies(userWallet.walletId) + currenciesToAdd) + .addMissingBlockchains(card) - withContext(Dispatchers.IO) { - userTokensRepository.saveUserTokens( - card = card, - tokens = newCurrencies, + updateWalletStores(userWallet, updatedBlockchainNetworks) + .map { + saveUserCurrencies(card, newCurrencies) + } + .flatMap { + walletAmountsRepository.updateAmountsForUserWallet( + userWallet = userWallet, + fiatCurrency = appCurrencyProvider(), ) } - } + } + + override suspend fun removeCurrencies( + userWallet: UserWallet, + currenciesToRemove: List, + ): CompletionResult = withContext(Dispatchers.Default) { + val card = userWallet.scanResponse.card + val remainingCurrencies = getSavedCurrencies(userWallet.walletId) + .filter { it !in currenciesToRemove } + val remainingBlockchains = remainingCurrencies + .filterIsInstance() + .map { it.blockchain } + + walletStoresRepository.deleteDifference(userWallet.walletId, remainingBlockchains) .flatMap { - newBlockchainNetworks.updateWalletStores(userWallet) + updateWalletStores(userWallet, remainingCurrencies.toBlockchainNetworks()) + } + .map { + saveUserCurrencies(card, remainingCurrencies) } } @@ -81,108 +98,69 @@ internal class DefaultWalletCurrenciesManager( return removeCurrencies(userWallet, listOf(currencyToRemove)) } - override suspend fun removeCurrencies( - userWallet: UserWallet, - currenciesToRemove: List, - ): CompletionResult = withContext(Dispatchers.Default) { - var remainingBlockchainsNetworks = emptyList() - catching { - val card = userWallet.scanResponse.card - val savedCurrencies = withContext(Dispatchers.IO) { - userTokensRepository.getUserTokens(card) - } - - val remainingCurrencies = arrayListOf() - savedCurrencies.forEach { savedCurrency -> - if (savedCurrency !in currenciesToRemove) { - remainingCurrencies.add(savedCurrency) + private suspend fun getSavedCurrencies(userWalletId: UserWalletId): List { + return withContext(Dispatchers.Default) { + walletStoresRepository.getSync(userWalletId) + .flatMap { walletStore -> + walletStore.walletsData.map { it.currency } } - } - - remainingBlockchainsNetworks = remainingCurrencies.toBlockchainNetworks(userWallet.scanResponse.card) - - withContext(Dispatchers.IO) { - userTokensRepository.saveUserTokens( - card = card, - tokens = remainingCurrencies, - ) - } } - .flatMap { - remainingBlockchainsNetworks.updateWalletStores(userWallet) - } } - private fun List.toBlockchainNetworks(card: CardDTO): List { - val blockchainNetworks = arrayListOf() - val findDerivationPath: (currency: Currency) -> String? = { currency -> - currency.derivationPath - ?: currency.blockchain.derivationPath(card.derivationStyle) - ?.rawPath + private suspend fun saveUserCurrencies(card: CardDTO, currencies: List) { + withContext(Dispatchers.IO) { + userTokensRepository.saveUserTokens( + card = card, + tokens = currencies, + ) } + } - for (currency in this.sortedByDescending { it.isBlockchain() }) { + private fun List.addMissingBlockchains(card: CardDTO): List { + val newCurrencies = arrayListOf() + + for (currency in this.sortedByDescending { it is Currency.Blockchain }) { when (currency) { is Currency.Blockchain -> { - val blockchainNetwork = BlockchainNetwork( - blockchain = currency.blockchain, - derivationPath = findDerivationPath(currency), - tokens = getTokens(currency), - ) - - blockchainNetworks.add(blockchainNetwork) + newCurrencies.add(currency.updateDerivationPath(card.derivationStyle)) } - is Currency.Token -> { - val tokenBlockchainNetworkIndex = blockchainNetworks - .indexOfFirst { - it.blockchain == currency.blockchain && - it.derivationPath == currency.derivationPath - } - if (tokenBlockchainNetworkIndex == -1) { - blockchainNetworks.add( - BlockchainNetwork( + is Currency.Token -> { + val containsTokenBlockchain = newCurrencies.any { + it.isBlockchain() && it.blockchain == currency.blockchain + } + + if (containsTokenBlockchain) { + newCurrencies.add(currency.updateDerivationPath(card.derivationStyle)) + } else { + val derivationPath = findDerivationPath(currency, card.derivationStyle) + newCurrencies.add( + Currency.Blockchain( blockchain = currency.blockchain, - derivationPath = findDerivationPath(currency), - tokens = listOf(currency.token), + derivationPath = derivationPath, ), ) - } else { - val tokenBlockchainNetwork = blockchainNetworks[tokenBlockchainNetworkIndex] - if (currency.token in tokenBlockchainNetwork.tokens) { - continue - } else { - blockchainNetworks.add( - tokenBlockchainNetworkIndex, - tokenBlockchainNetwork.copy( - tokens = tokenBlockchainNetwork.tokens + currency.token, - ), - ) - } + newCurrencies.add( + currency.copy(derivationPath = derivationPath), + ) } } } } - return blockchainNetworks + return newCurrencies } - private suspend fun List.updateWalletStores( + private suspend fun updateWalletStores( userWallet: UserWallet, + blockchainNetworks: List, ): CompletionResult { val userWalletId = userWallet.walletId - return this - .also { blockchainNetworks -> - walletStoresRepository.deleteDifference( - userWalletId = userWalletId, - currentBlockchains = blockchainNetworks.map { it.blockchain }, - ) - } + return blockchainNetworks .map { blockchainNetwork -> - walletManagersRepository.findOrMake( + walletManagersRepository.findOrMakeMultiCurrencyWalletManager( userWallet = userWallet, blockchainNetwork = blockchainNetwork, - refresh = true, ) .flatMap { walletManager -> walletStoresRepository.storeOrUpdate( @@ -194,11 +172,25 @@ internal class DefaultWalletCurrenciesManager( } } .fold() - .flatMap { - walletAmountsRepository.update( - userWallet = userWallet, - fiatCurrency = appCurrencyProvider(), - ) - } + } + + private fun Currency.updateDerivationPath(cardDerivationStyle: DerivationStyle?): Currency { + val findDerivationPath: () -> String? = { + findDerivationPath(this, cardDerivationStyle) + } + + return when (this) { + is Currency.Blockchain -> this.copy( + derivationPath = findDerivationPath(), + ) + + is Currency.Token -> this.copy( + derivationPath = findDerivationPath(), + ) + } + } + + private fun findDerivationPath(currency: Currency, cardDerivationStyle: DerivationStyle?): String? { + return currency.derivationPath ?: currency.blockchain.derivationPath(cardDerivationStyle)?.rawPath } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresManager.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresManager.kt index 346a6108e8..e834cf0541 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresManager.kt @@ -10,7 +10,7 @@ interface WalletStoresManager { fun getAll(): Flow>> fun get(userWalletId: UserWalletId): Flow> - suspend fun delete(userWalletsIds: List): CompletionResult + suspend fun delete(userWalletsIds: List): CompletionResult suspend fun clear(): CompletionResult suspend fun fetch( @@ -23,5 +23,9 @@ interface WalletStoresManager { refresh: Boolean = false, ): CompletionResult + suspend fun updateAmounts( + userWallets: List, + ): CompletionResult + companion object } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DefaultWalletStoresManager.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DefaultWalletStoresManager.kt index 041ea91557..219d7f4af1 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DefaultWalletStoresManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DefaultWalletStoresManager.kt @@ -2,19 +2,19 @@ package com.tangem.tap.domain.walletStores.implementation import com.tangem.blockchain.common.WalletManager import com.tangem.common.CompletionResult +import com.tangem.common.doOnSuccess import com.tangem.common.flatMap import com.tangem.common.flatMapOnFailure +import com.tangem.common.fold import com.tangem.common.map import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.common.entities.FiatCurrency -import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.domain.model.UserWallet import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.model.builders.WalletStoreBuilder import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.domain.walletStores.WalletStoresError import com.tangem.tap.domain.walletStores.WalletStoresManager -import com.tangem.tap.domain.walletStores.implementation.utils.fold import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository @@ -22,6 +22,7 @@ import com.tangem.tap.features.wallet.models.toBlockchainNetworks import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext @@ -40,12 +41,12 @@ internal class DefaultWalletStoresManager( override fun get(userWalletId: UserWalletId): Flow> { return walletStoresRepository.get(userWalletId) + .distinctUntilChanged() } - override suspend fun delete(userWalletsIds: List): CompletionResult { - val walletIds = userWalletsIds.map { UserWalletId(it) } - return walletStoresRepository.delete(walletIds) - .flatMap { walletManagersRepository.delete(walletIds) } + override suspend fun delete(userWalletsIds: List): CompletionResult { + return walletStoresRepository.delete(userWalletsIds) + .flatMap { walletManagersRepository.delete(userWalletsIds) } } override suspend fun clear(): CompletionResult { @@ -55,7 +56,7 @@ internal class DefaultWalletStoresManager( override suspend fun fetch( userWallets: List, refresh: Boolean, - ): CompletionResult { + ): CompletionResult = withContext(Dispatchers.Default) { val fiatCurrency = appCurrencyProvider.invoke() val isFiatCurrencyChanged = state.value.fiatCurrency != fiatCurrency @@ -65,18 +66,18 @@ internal class DefaultWalletStoresManager( ) } - return userWallets + userWallets .mapNotNull { userWallet -> val hasNotWalletStoresForUserWallet = !walletStoresRepository.contains(userWallet.walletId) if (refresh || hasNotWalletStoresForUserWallet || isFiatCurrencyChanged) { - fetchWalletsIfNeeded(userWallet, refresh) + fetchWalletsIfNeeded(userWallet) } else null } - .fold(initial = arrayListOf()) { acc, data -> + .fold(arrayListOf()) { acc, data -> acc.apply { add(data) } } .flatMap { - walletAmountsRepository.update(it, fiatCurrency) + walletAmountsRepository.updateAmountsForUserWallets(it, fiatCurrency) } } @@ -87,22 +88,29 @@ internal class DefaultWalletStoresManager( return fetch(listOf(userWallet), refresh) } - private suspend fun fetchWalletsIfNeeded( - userWallet: UserWallet, - refresh: Boolean, - ): CompletionResult { - return if (userWallet.scanResponse.card.isMultiwalletAllowed) { - fetchMultiWallets(userWallet, refresh) + override suspend fun updateAmounts(userWallets: List): CompletionResult { + val fiatCurrency = appCurrencyProvider.invoke() + + return walletAmountsRepository.updateAmountsForUserWallets(userWallets, fiatCurrency) + .doOnSuccess { + state.update { prevState -> + prevState.copy( + fiatCurrency = fiatCurrency, + ) + } + } + } + + private suspend fun fetchWalletsIfNeeded(userWallet: UserWallet): CompletionResult { + return if (userWallet.isMultiCurrency) { + fetchMultiWallets(userWallet) } else { - fetchSingleWallet(userWallet, refresh) + fetchSingleWallet(userWallet) } .map { userWallet } } - private suspend fun fetchMultiWallets( - userWallet: UserWallet, - refresh: Boolean, - ): CompletionResult { + private suspend fun fetchMultiWallets(userWallet: UserWallet): CompletionResult { val scanResponse = userWallet.scanResponse val userTokens = withContext(Dispatchers.IO) { userTokensRepository.getUserTokens(scanResponse.card) @@ -128,10 +136,9 @@ internal class DefaultWalletStoresManager( ) } - walletManagersRepository.findOrMake( + walletManagersRepository.findOrMakeMultiCurrencyWalletManager( userWallet = userWallet, blockchainNetwork = blockchainNetwork, - refresh = refresh, ) .flatMap { walletManager -> storeWalletStore(walletManager) @@ -149,13 +156,9 @@ internal class DefaultWalletStoresManager( } } - private suspend fun fetchSingleWallet( - userWallet: UserWallet, - refresh: Boolean, - ): CompletionResult { - return walletManagersRepository.findOrMake( + private suspend fun fetchSingleWallet(userWallet: UserWallet): CompletionResult { + return walletManagersRepository.findOrMakeSingleCurrencyWalletManager( userWallet = userWallet, - refresh = refresh, ) .flatMap { walletManager -> val userWalletId = userWallet.walletId diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DummyWalletStoresManager.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DummyWalletStoresManager.kt index 93c5c780c1..b1729a0b11 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DummyWalletStoresManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DummyWalletStoresManager.kt @@ -17,7 +17,7 @@ internal class DummyWalletStoresManager : WalletStoresManager { return emptyFlow() } - override suspend fun delete(userWalletsIds: List): CompletionResult { + override suspend fun delete(userWalletsIds: List): CompletionResult { return CompletionResult.Success(Unit) } @@ -32,4 +32,8 @@ internal class DummyWalletStoresManager : WalletStoresManager { override suspend fun fetch(userWallets: List, refresh: Boolean): CompletionResult { return CompletionResult.Success(Unit) } + + override suspend fun updateAmounts(userWallets: List): CompletionResult { + return CompletionResult.Success(Unit) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/utils/CompletitionsResultOperations.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/utils/CompletitionsResultOperations.kt deleted file mode 100644 index 3c94784ec8..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/utils/CompletitionsResultOperations.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.tap.domain.walletStores.implementation.utils - -import com.tangem.common.CompletionResult - -internal fun List>.fold(): CompletionResult { - return fold(Unit) { _, _ -> Unit } -} - -@Suppress("UNCHECKED_CAST") -internal inline fun List>.fold( - initial: R, - operation: (acc: R, data: D) -> R, -): CompletionResult { - var resultData = initial - for (result in this) { - when (result) { - is CompletionResult.Success -> { - resultData = operation(resultData, result.data) - } - is CompletionResult.Failure -> { - return result as CompletionResult.Failure - } - } - } - - return CompletionResult.Success(resultData) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletAmountsRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletAmountsRepository.kt index f46a552dfe..0af5b8f85e 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletAmountsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletAmountsRepository.kt @@ -6,19 +6,39 @@ import com.tangem.tap.domain.model.UserWallet import com.tangem.tap.domain.model.WalletStoreModel interface WalletAmountsRepository { - suspend fun update( + + /** + * Fetch wallet amounts and fiat rates then update [com.tangem.tap.domain.walletStores.storage.WalletStoresStorage] + * and [com.tangem.tap.domain.walletStores.storage.WalletManagerStorage] with new data + * @param userWallets list of [UserWallet] which will be used to get the list of associated [WalletStoreModel] + * @param fiatCurrency current app [FiatCurrency] + * */ + suspend fun updateAmountsForUserWallets( userWallets: List, fiatCurrency: FiatCurrency, ): CompletionResult - suspend fun update( + /** + * Fetch wallet amounts and fiat rates then update [com.tangem.tap.domain.walletStores.storage.WalletStoresStorage] + * and [com.tangem.tap.domain.walletStores.storage.WalletManagerStorage] with new data + * @param userWallet [UserWallet] which will be used to get the list of associated [WalletStoreModel] + * @param fiatCurrency current app [FiatCurrency] + * */ + suspend fun updateAmountsForUserWallet( userWallet: UserWallet, fiatCurrency: FiatCurrency, ): CompletionResult - suspend fun update( - userWallet: UserWallet, + /** + * Fetch wallet amounts and fiat rates then update [com.tangem.tap.domain.walletStores.storage.WalletStoresStorage] + * and [com.tangem.tap.domain.walletStores.storage.WalletManagerStorage] with new data + * @param walletStore [WalletStoreModel] to update + * @param userWallet [UserWallet] associated with provided [walletStore] + * @param fiatCurrency current app [FiatCurrency] + * */ + suspend fun updateAmountsForWalletStore( walletStore: WalletStoreModel, + userWallet: UserWallet, fiatCurrency: FiatCurrency, ): CompletionResult diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletManagersRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletManagersRepository.kt index 9c8f9b1c13..0d8a2044be 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletManagersRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletManagersRepository.kt @@ -8,12 +8,13 @@ import com.tangem.tap.domain.model.UserWallet import com.tangem.tap.domain.tokens.models.BlockchainNetwork interface WalletManagersRepository { - suspend fun findOrMake( + suspend fun findOrMakeMultiCurrencyWalletManager( userWallet: UserWallet, - blockchainNetwork: BlockchainNetwork? = null, - refresh: Boolean = false, + blockchainNetwork: BlockchainNetwork, ): CompletionResult + suspend fun findOrMakeSingleCurrencyWalletManager(userWallet: UserWallet): CompletionResult + suspend fun delete(userWalletIds: List): CompletionResult suspend fun delete( diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletStoresRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletStoresRepository.kt index a5184fd40f..7b07a6eee0 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletStoresRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletStoresRepository.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.Flow interface WalletStoresRepository { fun getAll(): Flow>> fun get(userWalletId: UserWalletId): Flow> + suspend fun getSync(userWalletId: UserWalletId): List suspend fun contains(userWalletId: UserWalletId): Boolean diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt index fd8a037dba..640c08fe67 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt @@ -9,9 +9,10 @@ import com.tangem.blockchain.extensions.Result.Failure import com.tangem.blockchain.extensions.Result.Success import com.tangem.common.CompletionResult import com.tangem.common.catching -import com.tangem.common.doOnSuccess +import com.tangem.common.core.TangemError import com.tangem.common.flatMap import com.tangem.common.flatMapOnFailure +import com.tangem.common.fold import com.tangem.common.map import com.tangem.common.services.Result import com.tangem.datasource.api.tangemTech.TangemTechService @@ -22,9 +23,9 @@ import com.tangem.tap.common.extensions.replaceByOrAdd import com.tangem.tap.domain.model.UserWallet import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.walletStores.WalletStoresError -import com.tangem.tap.domain.walletStores.implementation.utils.fold import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository import com.tangem.tap.domain.walletStores.repository.implementation.utils.replaceWalletStore +import com.tangem.tap.domain.walletStores.repository.implementation.utils.replaceWalletStores import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithAmounts import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithError import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithFiatRates @@ -40,7 +41,7 @@ import com.tangem.tap.network.NetworkConnectivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import timber.log.Timber import java.math.BigDecimal @@ -51,30 +52,30 @@ internal class DefaultWalletAmountsRepository( private val walletStoresStorage = WalletStoresStorage private val walletManagersStorage = WalletManagerStorage - override suspend fun update( + override suspend fun updateAmountsForUserWallets( userWallets: List, fiatCurrency: FiatCurrency, ): CompletionResult { return if (userWallets.isEmpty()) CompletionResult.Success(Unit) else withContext(Dispatchers.Default) { awaitAll( - async { fetchAmounts(userWallets) }, + async { fetchAmountsForUserWallets(userWallets) }, async { fetchFiatRates(userWallets, fiatCurrency) }, ) .fold() } } - override suspend fun update( + override suspend fun updateAmountsForUserWallet( userWallet: UserWallet, fiatCurrency: FiatCurrency, ): CompletionResult { - return update(listOf(userWallet), fiatCurrency) + return updateAmountsForUserWallets(listOf(userWallet), fiatCurrency) } - override suspend fun update( - userWallet: UserWallet, + override suspend fun updateAmountsForWalletStore( walletStore: WalletStoreModel, + userWallet: UserWallet, fiatCurrency: FiatCurrency, ): CompletionResult = withContext(Dispatchers.Default) { val walletId = userWallet.walletId @@ -84,33 +85,18 @@ internal class DefaultWalletAmountsRepository( async { // TODO: Find wallet manager via [com.tangem.tap.domain.walletStores.repository.WalletManagersRepository] val walletManager = walletStore.walletManager - fetchAmounts(walletId, scanResponse, walletStore, walletManager) - .flatMap { fetchRentIfNeeded(walletStore, walletManager) } + fetchAmountsForWalletStore(walletId, scanResponse, walletStore, walletManager) }, async { fetchFiatRates(listOf(userWallet), fiatCurrency) }, ) .fold() } - private suspend fun fetchAmounts( + private suspend fun fetchAmountsForUserWallets( userWallets: List, - ): CompletionResult = coroutineScope { - userWallets.map { userWallet -> - val walletId = userWallet.walletId - val scanResponse = userWallet.scanResponse - val walletStores = walletStoresStorage.getSync(walletId) - - walletStores.map { walletStore -> - async { - // TODO: Find wallet manager via [com.tangem.tap.domain.walletStores.repository.WalletManagersRepository] - val walletManager = walletStore.walletManager - fetchAmounts(walletId, scanResponse, walletStore, walletManager) - .flatMap { fetchRentIfNeeded(walletStore, walletManager) } - } - } - .awaitAll() - .fold() - } + ): CompletionResult = withContext(Dispatchers.Default) { + userWallets.map { async { fetchAmountsForUserWallet(it) } } + .awaitAll() .fold() } @@ -120,7 +106,11 @@ internal class DefaultWalletAmountsRepository( ): CompletionResult { val walletsIds = userWallets.map { it.walletId } val walletStores = walletsIds - .flatMap { walletStoresStorage.getSync(it) } + .flatMap { + walletStoresStorage.getAll() + .first() + .getOrElse(it) { emptyList() } + } val currencies = walletStores .asSequence() @@ -138,21 +128,11 @@ internal class DefaultWalletAmountsRepository( return when (fiatRatesResult) { is Result.Success -> { - Timber.d( - """ - Fetched fiat rates - |- User wallets ids: $walletsIds - |- Coins ids: $coinsIds - """.trimIndent(), + updateWalletStoresWithFiatRates( + walletStores = walletStores, + fiatRates = fiatRatesResult.data.rates, ) - walletStores.forEach { walletStore -> - updateWithFiatRates( - walletStore = walletStore, - fiatRates = fiatRatesResult.data.rates, - ) - } - CompletionResult.Success(Unit) } is Result.Failure -> { @@ -175,102 +155,66 @@ internal class DefaultWalletAmountsRepository( } } - private suspend fun fetchAmounts( + private suspend fun fetchAmountsForUserWallet( + userWallet: UserWallet, + ): CompletionResult = withContext(Dispatchers.Default) { + val walletId = userWallet.walletId + val scanResponse = userWallet.scanResponse + val walletStores = walletStoresStorage.getAll() + .first() + .getOrElse(walletId) { emptyList() } + + walletStores.map { walletStore -> + async { + // TODO: Find wallet manager via [com.tangem.tap.domain.walletStores.repository.WalletManagersRepository] + val walletManager = walletStore.walletManager + fetchAmountsForWalletStore(walletId, scanResponse, walletStore, walletManager) + } + } + .awaitAll() + .fold() + } + + private suspend fun fetchAmountsForWalletStore( walletId: UserWalletId, scanResponse: ScanResponse, walletStore: WalletStoreModel, walletManager: WalletManager?, ): CompletionResult { - val hasMissedDerivations = with(walletStore.blockchainNetwork) { - derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath) + val hasMissedDerivations = with(walletStore) { + derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath.rawPath) } - val blockchain = walletStore.blockchainNetwork.blockchain - val tokens = walletStore.blockchainNetwork.tokens.map { it.name } return when { - hasMissedDerivations -> { - Timber.e( - """ - Missed derivation - |- User wallet id: $walletId - |- Blockchain: $blockchain - """.trimIndent(), - ) - - updateWithMissedDerivation( - walletStore = walletStore, - ) - - CompletionResult.Success(Unit) - } - walletManager == null -> { - Timber.e( - """ - Wallet manager is null - |- User wallet id: $walletId - |- Blockchain: $blockchain - """.trimIndent(), - ) - - updateWithUnreachable( - walletStore = walletStore, - ) - - CompletionResult.Success(Unit) - } + hasMissedDerivations -> updateWalletStoreWithMissedDerivation(walletStore) + walletManager == null -> updateWalletStoreWithUnreachable(walletStore) else -> { withInternetConnection { walletManager.update() } .map { updateWalletManagerWithAmounts(walletId, walletManager) } - .doOnSuccess { - Timber.d( - """ - Fetched amounts - |- User wallet id: $walletId - |- Blockchain: $blockchain - |- Tokens: $tokens - """.trimIndent(), - ) - - updateWithAmounts( + .flatMap { + updateWalletStoreWithAmounts( walletStore = walletStore, - wallet = walletManager.wallet, + updatedWallet = walletManager.wallet, ) } + .flatMap { fetchWalletStoreRentIfNeeded(walletStore, walletManager) } .flatMapOnFailure { error -> - Timber.e( - error, - """ - Unable to fetch amounts - |- User wallet id: $walletId - |- Blockchain: $blockchain - |- Tokens: $tokens - """.trimIndent(), + updateWalletStoreWithError( + walletStore = walletStore, + wallet = walletManager.wallet, + error = error, ) - - if (error is BlockchainSdkError) { - updateWithError( - walletStore = walletStore, - wallet = walletManager.wallet, - error = error, - ) - CompletionResult.Success(Unit) - } else { - CompletionResult.Failure(error) - } } } } } - private suspend fun fetchRentIfNeeded( + private suspend fun fetchWalletStoreRentIfNeeded( walletStore: WalletStoreModel, - walletManager: WalletManager?, + walletManager: WalletManager, ): CompletionResult { val rentProvider = walletManager as? RentProvider - - if (walletManager == null || rentProvider == null) { - return CompletionResult.Success(Unit) - } + ?: return CompletionResult.Success(Unit) when (val result = rentProvider.minimalBalanceForRentExemption()) { is Success -> { @@ -288,7 +232,7 @@ internal class DefaultWalletAmountsRepository( balance < rest } - updateWithRent( + updateWalletStoreWithRent( walletStore = walletStore, rent = if (setRent) { WalletStoreModel.WalletRent( @@ -333,76 +277,123 @@ internal class DefaultWalletAmountsRepository( } } - private suspend fun updateWithError( + private suspend fun updateWalletStoreWithError( walletStore: WalletStoreModel, wallet: Wallet, - error: BlockchainSdkError, + error: TangemError, ) = withContext(Dispatchers.Default) { - walletStoresStorage.update { prevState -> - prevState.replaceWalletStore( - walletId = walletStore.userWalletId, - walletStore = walletStore, - update = { - it.updateWithError( - wallet = wallet, - error = error, - ) - }, - ) + Timber.e( + error, + """ + Unable to fetch amounts + |- User wallet id: ${walletStore.userWalletId} + |- Blockchain: ${walletStore.blockchain} + """.trimIndent(), + ) + + if (error is BlockchainSdkError) { + walletStoresStorage.update { prevState -> + prevState.replaceWalletStore( + walletStoreToUpdate = walletStore, + update = { + it.updateWithError( + wallet = wallet, + error = error, + ) + }, + ) + } + + CompletionResult.Success(Unit) + } else { + CompletionResult.Failure(error) } } - private suspend fun updateWithAmounts( + private suspend fun updateWalletStoreWithAmounts( walletStore: WalletStoreModel, - wallet: Wallet, + updatedWallet: Wallet, ) = withContext(Dispatchers.Default) { + Timber.d( + """ + Fetched amounts + |- User wallet id: ${walletStore.userWalletId} + |- Blockchain: ${walletStore.blockchain} + """.trimIndent(), + ) + walletStoresStorage.update { prevState -> prevState.replaceWalletStore( - walletId = walletStore.userWalletId, - walletStore = walletStore, + walletStoreToUpdate = walletStore, update = { - it.updateWithAmounts(wallet = wallet) + it.updateWithAmounts(wallet = updatedWallet) }, ) } + + CompletionResult.Success(Unit) } - private suspend fun updateWithMissedDerivation( + private suspend fun updateWalletStoreWithMissedDerivation( walletStore: WalletStoreModel, ) = withContext(Dispatchers.Default) { + Timber.e( + """ + Missed derivation + |- User wallet id: ${walletStore.userWalletId} + |- Blockchain: ${walletStore.blockchain} + """.trimIndent(), + ) + walletStoresStorage.update { prevState -> prevState.replaceWalletStore( - walletId = walletStore.userWalletId, - walletStore = walletStore, + walletStoreToUpdate = walletStore, update = { it.updateWithMissedDerivation() }, ) } + + CompletionResult.Success(Unit) } - private suspend fun updateWithUnreachable( + private suspend fun updateWalletStoreWithUnreachable( walletStore: WalletStoreModel, ) = withContext(Dispatchers.Default) { + Timber.e( + """ + Wallet manager is null + |- User wallet id: ${walletStore.userWalletId} + |- Blockchain: ${walletStore.blockchain} + """.trimIndent(), + ) + walletStoresStorage.update { prevState -> prevState.replaceWalletStore( - walletId = walletStore.userWalletId, - walletStore = walletStore, + walletStoreToUpdate = walletStore, update = { it.updateWithUnreachable() }, ) } + + CompletionResult.Success(Unit) } - private suspend fun updateWithFiatRates( - walletStore: WalletStoreModel, + private suspend fun updateWalletStoresWithFiatRates( + walletStores: List, fiatRates: Map, ) = withContext(Dispatchers.Default) { + Timber.d( + """ + Fetched fiat rates + |- User wallets ids: ${walletStores.map { it.userWalletId }.distinct()} + """.trimIndent(), + ) + walletStoresStorage.update { prevState -> - prevState.replaceWalletStore( - walletId = walletStore.userWalletId, - walletStore = walletStore, + prevState.replaceWalletStores( + walletStoresToUpdate = walletStores, update = { it.updateWithFiatRates(rates = fiatRates) }, @@ -410,18 +401,28 @@ internal class DefaultWalletAmountsRepository( } } - private suspend fun updateWithRent( + private suspend fun updateWalletStoreWithRent( walletStore: WalletStoreModel, rent: WalletStoreModel.WalletRent?, ) = withContext(Dispatchers.Default) { - walletStoresStorage.update { prevState -> - prevState.replaceWalletStore( - walletId = walletStore.userWalletId, - walletStore = walletStore, - update = { - it.updateWithRent(rent) - }, - ) + Timber.d( + """ + Fetched wallet rent + |- User wallet id: ${walletStore.userWalletId} + |- Blockchain: ${walletStore.blockchain} + |- Rent: $rent + """.trimIndent(), + ) + + if (rent != walletStore.walletRent) { + walletStoresStorage.update { prevState -> + prevState.replaceWalletStore( + walletStoreToUpdate = walletStore, + update = { + it.updateWithRent(rent) + }, + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt index 600120ad42..3de7a3f6b1 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt @@ -1,6 +1,10 @@ package com.tangem.tap.domain.walletStores.repository.implementation -import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.DerivationParams +import com.tangem.blockchain.common.DerivationStyle +import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.common.CompletionResult import com.tangem.common.catching import com.tangem.common.hdWallet.DerivationPath @@ -18,6 +22,7 @@ import com.tangem.tap.domain.walletStores.WalletStoresError import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext import timber.log.Timber @@ -26,26 +31,31 @@ internal class DefaultWalletManagersRepository( ) : WalletManagersRepository { private val walletManagersStorage = WalletManagerStorage - override suspend fun findOrMake( + override suspend fun findOrMakeMultiCurrencyWalletManager( + userWallet: UserWallet, + blockchainNetwork: BlockchainNetwork, + ): CompletionResult { + return findOrMakeInternal(userWallet, blockchainNetwork) + } + + override suspend fun findOrMakeSingleCurrencyWalletManager(userWallet: UserWallet): CompletionResult { + return findOrMakeInternal(userWallet, blockchainNetwork = null) + } + + private suspend fun findOrMakeInternal( userWallet: UserWallet, blockchainNetwork: BlockchainNetwork?, - refresh: Boolean, ): CompletionResult = withContext(Dispatchers.Default) { - if (refresh) { - deleteInternal(userWallet.walletId, blockchainNetwork?.blockchain) - makeAndStore(userWallet, blockchainNetwork) - } else { - val foundWalletManager = findWalletManager( - userWalletId = userWallet.walletId, - blockchain = blockchainNetwork?.blockchain, - ) + val foundWalletManager = findWalletManager( + userWalletId = userWallet.walletId, + blockchain = blockchainNetwork?.blockchain, + ) - foundWalletManager?.updateTokens( - scanResponse = userWallet.scanResponse, - blockchainNetwork = blockchainNetwork, - ) - ?: makeAndStore(userWallet, blockchainNetwork) - } + foundWalletManager?.updateTokens( + scanResponse = userWallet.scanResponse, + blockchainNetwork = blockchainNetwork, + ) + ?: makeAndStore(userWallet, blockchainNetwork) } private suspend fun makeAndStore( @@ -150,8 +160,11 @@ internal class DefaultWalletManagersRepository( return catching { val tokens = blockchainNetwork?.tokens ?: listOfNotNull(scanResponse.getPrimaryToken()) - if (tokens.isNotEmpty()) { - walletManager.addTokens(tokens) + if (tokens != cardTokens) { + cardTokens.clear() + if (tokens.isNotEmpty()) { + walletManager.cardTokens.addAll(tokens) + } } walletManager @@ -170,7 +183,7 @@ internal class DefaultWalletManagersRepository( userWalletId: UserWalletId, blockchain: Blockchain?, ): WalletManager? { - return walletManagersStorage.getAllSync()[userWalletId]?.let { userWalletManagers -> + return walletManagersStorage.getAll().first()[userWalletId]?.let { userWalletManagers -> if (blockchain == null) userWalletManagers.firstOrNull() else userWalletManagers.find { it.wallet.blockchain == blockchain } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletStoresRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletStoresRepository.kt index f12cd945cf..b9087070e4 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletStoresRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletStoresRepository.kt @@ -12,6 +12,8 @@ import com.tangem.tap.domain.walletStores.repository.implementation.utils.update import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext internal class DefaultWalletStoresRepository : WalletStoresRepository { @@ -22,11 +24,15 @@ internal class DefaultWalletStoresRepository : WalletStoresRepository { } override fun get(userWalletId: UserWalletId): Flow> { - return walletStoresStorage.get(userWalletId) + return getAll().map { it[userWalletId].orEmpty() } + } + + override suspend fun getSync(userWalletId: UserWalletId): List { + return get(userWalletId).firstOrNull() ?: emptyList() } override suspend fun contains(userWalletId: UserWalletId): Boolean { - return walletStoresStorage.getSync(userWalletId).isNotEmpty() + return getSync(userWalletId).isNotEmpty() } override suspend fun delete(userWalletsIds: List): CompletionResult = catching { @@ -39,11 +45,13 @@ internal class DefaultWalletStoresRepository : WalletStoresRepository { userWalletId: UserWalletId, currentBlockchains: List, ): CompletionResult = catching { - walletStoresStorage.update { prevStores -> - prevStores.apply { - this[userWalletId] = this[userWalletId] - ?.filter { it.blockchainNetwork.blockchain in currentBlockchains } - .orEmpty() + if (currentBlockchains != getSync(userWalletId).map { it.blockchain }) { + walletStoresStorage.update { prevStores -> + prevStores.apply { + this[userWalletId] = this[userWalletId] + ?.filter { it.blockchain in currentBlockchains } + .orEmpty() + } } } } @@ -65,24 +73,22 @@ internal class DefaultWalletStoresRepository : WalletStoresRepository { userWalletId: UserWalletId, walletStore: WalletStoreModel, ): HashMap> = withContext(Dispatchers.Default) { - val prevStores = this@addOrUpdate - val walletStores = prevStores[userWalletId] + val currentWalletStores = this@addOrUpdate + val userWalletStores = currentWalletStores[userWalletId] - if (walletStores.isNullOrEmpty()) { - prevStores.apply { + if (userWalletStores.isNullOrEmpty()) { + currentWalletStores.apply { set(userWalletId, listOf(walletStore)) } } else { - val oldWalletStore = walletStores.find(walletStore::isSameWalletStore) - - if (oldWalletStore == null) { - prevStores.apply { - set(userWalletId, walletStores + walletStore) + val currentWalletStore = userWalletStores.find(walletStore::isSameWalletStore) + if (currentWalletStore == null) { + currentWalletStores.apply { + set(userWalletId, userWalletStores + walletStore) } } else { - prevStores.replaceWalletStore( - walletId = userWalletId, - walletStore = oldWalletStore, + currentWalletStores.replaceWalletStore( + walletStoreToUpdate = currentWalletStore, update = { it.updateWithSelf(walletStore) }, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt index 39d4adfadc..7ad0e38363 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt @@ -125,10 +125,9 @@ internal fun WalletDataModel.updateWithSelf( status = when (val newStatus = newWalletData.status) { is WalletDataModel.Loading -> when (oldStatus) { is WalletDataModel.MissedDerivation -> WalletDataModel.Loading - else -> oldStatus.asRefreshing() + else -> oldStatus } is WalletDataModel.MissedDerivation, - is WalletDataModel.Refreshing, is WalletDataModel.NoAccount, is WalletDataModel.Unreachable, is WalletDataModel.SameCurrencyTransactionInProgress, diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt index 8d29c62825..445ff655f2 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt @@ -4,16 +4,28 @@ import com.tangem.blockchain.common.Wallet import com.tangem.common.core.TangemError import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.domain.model.WalletStoreModel +import timber.log.Timber internal inline fun HashMap>.replaceWalletStore( - walletId: UserWalletId, - walletStore: WalletStoreModel, + walletStoreToUpdate: WalletStoreModel, + update: (walletStore: WalletStoreModel) -> WalletStoreModel, +): HashMap> { + return replaceWalletStores(listOf(walletStoreToUpdate), update) +} + +internal inline fun HashMap>.replaceWalletStores( + walletStoresToUpdate: List, update: (walletStore: WalletStoreModel) -> WalletStoreModel, ): HashMap> { return this.apply { - this[walletId] = this[walletId] - ?.replaceWalletStore(walletStore, update) - .orEmpty() + val currentWalletStores = this + walletStoresToUpdate + .groupBy { it.userWalletId } + .forEach { (userWalletId, walletStoresToUpdate) -> + currentWalletStores[userWalletId] = currentWalletStores[userWalletId] + ?.replaceWalletStores(walletStoresToUpdate, update) + .orEmpty() + } } } @@ -74,20 +86,39 @@ internal fun WalletStoreModel.updateWithRent(rent: WalletStoreModel.WalletRent?) ) } -internal inline fun List.replaceWalletStore( - newWalletStore: WalletStoreModel, - update: (walletStore: WalletStoreModel) -> WalletStoreModel, -): List { - val mutableStores = ArrayList(this) - for ((index, walletStore) in this.withIndex()) { - if (walletStore.isSameWalletStore(newWalletStore)) { - mutableStores[index] = update(walletStore) - break - } - } - return mutableStores +internal fun WalletStoreModel.isSameWalletStore(other: WalletStoreModel): Boolean { + return this.blockchain == other.blockchain && + this.derivationPath == other.derivationPath } -internal fun WalletStoreModel.isSameWalletStore(other: WalletStoreModel): Boolean { - return blockchainNetwork == other.blockchainNetwork +private inline fun List.replaceWalletStores( + walletStoresToUpdate: List, + update: (walletStore: WalletStoreModel) -> WalletStoreModel, +): List { + val mutableStores = ArrayList(this) + + walletStoresToUpdate + .asSequence() + .map { it.blockchain to it.derivationPath } + .forEach { (blockchain, derivationPath) -> + val index = mutableStores.indexOfFirst { + it.blockchain == blockchain && it.derivationPath == derivationPath + } + val currentWalletStore = mutableStores[index] + val updatedWalletStore = update(currentWalletStore) + + if (currentWalletStore != updatedWalletStore) { + Timber.d( + """ + Update wallet store in storage + |- User wallet ID: ${updatedWalletStore.userWalletId} + |- Blockchain: ${updatedWalletStore.blockchain} + """.trimIndent(), + ) + + mutableStores[index] = updatedWalletStore + } + } + + return mutableStores } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/storage/WalletManagerStorage.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/storage/WalletManagerStorage.kt index 1dae6db10e..3646e38f6a 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/storage/WalletManagerStorage.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/storage/WalletManagerStorage.kt @@ -4,6 +4,8 @@ import com.tangem.blockchain.common.WalletManager import com.tangem.domain.common.util.UserWalletId import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -16,8 +18,8 @@ internal object WalletManagerStorage { managers.tryEmit(hashMapOf()) } - suspend fun getAllSync(): Map> { - return managers.first() + fun getAll(): SharedFlow>> { + return managers.asSharedFlow() } private val mutex = Mutex() diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/storage/WalletStoresStorage.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/storage/WalletStoresStorage.kt index d4b6061dfc..93cdfb508f 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/storage/WalletStoresStorage.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/storage/WalletStoresStorage.kt @@ -2,12 +2,11 @@ package com.tangem.tap.domain.walletStores.storage import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.domain.model.WalletStoreModel -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -19,20 +18,8 @@ internal object WalletStoresStorage { stores.tryEmit(hashMapOf()) } - fun getAll(): Flow>> { - return stores - } - - @OptIn(ExperimentalCoroutinesApi::class) - fun get(userWalletId: UserWalletId): Flow> { - return stores - .mapLatest { stores -> - stores[userWalletId].orEmpty() - } - } - - suspend fun getSync(userWalletId: UserWalletId): List { - return stores.first().getOrElse(userWalletId) { emptyList() } + fun getAll(): SharedFlow>> { + return stores.asSharedFlow() } private val mutex = Mutex() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 829103e4e7..319071642c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -4,6 +4,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess +import com.tangem.common.extensions.toHexString import com.tangem.common.flatMap import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.util.userWalletId @@ -61,7 +62,7 @@ class DetailsMiddleware { is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action) is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(state, action) is DetailsAction.ShowDisclaimer -> { - val uri = store.state.detailsState.cardTermsOfUseUrl + val uri = state.cardTermsOfUseUrl if (uri != null) { store.dispatch(NavigationAction.OpenDocument(uri)) } @@ -71,19 +72,26 @@ class DetailsMiddleware { store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) } is DetailsAction.CreateBackup -> { - store.state.detailsState.scanResponse?.let { + state.scanResponse?.let { store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false)) store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) } } DetailsAction.ScanCard -> { scope.launch { - tangemSdkManager.scanCard(cardId = state.scanResponse?.card?.cardId) + tangemSdkManager.scanCard(allowRequestAccessCodeFromRepository = true) .doOnSuccess { card -> - val currentCardId = store.state.globalState.scanResponse?.card - ?.userWalletId - ?.stringValue - if (card.userWalletId.stringValue == currentCardId) { + val isSameWallet = state.scanResponse?.card?.userWalletId + ?.equals(card.userWalletId) + ?: false + + // !!! Workaround !!! + // TODO: Remove after [REDACTED_JIRA] + val isTwinned = card.wallets.firstOrNull()?.publicKey?.toHexString() + ?.equals(state.scanResponse?.secondTwinPublicKey) + ?: false + + if (isSameWallet || isTwinned) { store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(card)) } else { store.dispatchDialogShow( @@ -116,6 +124,7 @@ class DetailsMiddleware { scope.launch { tangemSdkManager.resetToFactorySettings(card.cardId) .flatMap { userWalletsListManager.delete(listOf(card.userWalletId)) } + .flatMap { tangemSdkManager.deleteSavedUserCodes(setOf(card.cardId)) } .doOnSuccess { Analytics.send(Settings.CardSettings.FactoryResetFinished()) @@ -255,10 +264,10 @@ class DetailsMiddleware { private fun toggleSaveAccessCodes(state: DetailsState, enable: Boolean) = scope.launch { if (state.saveAccessCodes == enable) return@launch if (enable) { + saveAccessCodes(state) if (!state.saveWallets) { saveCurrentWallet(state) } - saveAccessCodes(state) } else { deleteSavedAccessCodes() } @@ -334,12 +343,15 @@ class DetailsMiddleware { useBiometricsForAccessCode = false, ) - store.dispatchOnMain( - DetailsAction.AppSettings.SwitchPrivacySetting.Success( - setting = PrivacySetting.SaveAccessCode, - enable = false, - ), - ) + tangemSdkManager.clearSavedUserCodes() + .doOnSuccess { + store.dispatchOnMain( + DetailsAction.AppSettings.SwitchPrivacySetting.Success( + setting = PrivacySetting.SaveAccessCode, + enable = false, + ), + ) + } } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index 6158d9bf38..0b1063c381 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -15,8 +15,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.layout.ContentScale @@ -38,10 +36,6 @@ fun CardSettingsScreen( modifier: Modifier = Modifier, ) { val needReadCard = state.cardDetails == null - val backgroundColor by rememberUpdatedState( - newValue = if (needReadCard) TangemTheme.colors.background.primary - else TangemTheme.colors.background.secondary, - ) SettingsScreensScaffold( content = { @@ -52,7 +46,7 @@ fun CardSettingsScreen( } }, titleRes = R.string.card_settings_title, - backgroundColor = backgroundColor, + backgroundColor = TangemTheme.colors.background.secondary, onBackClick = onBackPressed, ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt index 47946d1e73..1346fa4f24 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt @@ -25,6 +25,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.tap.common.compose.TangemTypography import com.tangem.tap.features.details.ui.common.ScreenTitle import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold @@ -36,6 +38,10 @@ fun DetailsScreen( onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { + SystemBarsEffect { + setSystemBarsColor(color = TangemColorPalette.Light1) + } + SettingsScreensScaffold( content = { Content(state = state, modifier = modifier) }, onBackClick = onBackPressed, diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt index ca82ff1fee..5461c66441 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt @@ -9,9 +9,19 @@ sealed class DisclaimerAction : Action { val type: DisclaimerType, ) : DisclaimerAction() - data class Show(val onAcceptCallback: VoidCallback? = null) : DisclaimerAction() + data class Show( + val onAcceptCallback: VoidCallback? = null, + val onDismissCallback: VoidCallback? = null, + ) : DisclaimerAction() + data class AcceptDisclaimer(val type: DisclaimerType) : DisclaimerAction() internal data class UpdateState(val type: DisclaimerType, val accepted: Boolean) : DisclaimerAction() - internal data class SetOnAcceptCallback(val onAcceptCallback: VoidCallback? = null) : DisclaimerAction() + + internal data class SetCallbacks( + val onAcceptCallback: VoidCallback? = null, + val onDismissCallback: VoidCallback? = null, + ) : DisclaimerAction() + + object OnBackPressed : DisclaimerAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt index 1b42d7b4fc..01e5de971a 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt @@ -28,7 +28,7 @@ private fun handleDisclaimerMiddleware(action: Action, appState: AppState) { } is DisclaimerAction.Show -> { handleUpdateState = state.type.createUpdateState() - store.dispatch(DisclaimerAction.SetOnAcceptCallback(action.onAcceptCallback)) + store.dispatch(DisclaimerAction.SetCallbacks(action.onAcceptCallback, action.onDismissCallback)) store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer)) } is DisclaimerAction.AcceptDisclaimer -> { @@ -36,7 +36,13 @@ private fun handleDisclaimerMiddleware(action: Action, appState: AppState) { handleUpdateState = action.type.createUpdateState() store.dispatch(NavigationAction.PopBackTo()) state.onAcceptCallback?.invoke() - store.dispatch(DisclaimerAction.SetOnAcceptCallback(null)) + store.dispatch(DisclaimerAction.SetCallbacks(null, null)) + } + is DisclaimerAction.OnBackPressed -> { + state.onDismissCallback?.invoke() + store.dispatch(DisclaimerAction.SetCallbacks(null, null)) + store.dispatch(NavigationAction.PopBackTo()) + } } } diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt index 1b282877f2..766c6a42ca 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt @@ -19,8 +19,9 @@ private fun internalReduce(action: Action, state: AppState): DisclaimerState { type = action.type, accepted = action.accepted, ) - is DisclaimerAction.SetOnAcceptCallback -> disclaimerState.copy( + is DisclaimerAction.SetCallbacks -> disclaimerState.copy( onAcceptCallback = action.onAcceptCallback, + onDismissCallback = action.onDismissCallback, ) else -> disclaimerState } diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt index ee0700f5b5..d7ca859973 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt @@ -11,6 +11,7 @@ data class DisclaimerState( val accepted: Boolean = false, val type: DisclaimerType = DisclaimerType.Tangem, val onAcceptCallback: VoidCallback? = null, + val onDismissCallback: VoidCallback? = null, ) : StateType sealed class DisclaimerType( diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt index 93d93a8a2e..282c5fe0e0 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt @@ -4,6 +4,8 @@ import android.os.Bundle import android.view.View import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding +import com.tangem.core.ui.fragments.setStatusBarColor +import com.tangem.tap.common.extensions.configureSettings import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.BaseFragment @@ -32,6 +34,7 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs override fun onStart() { super.onStart() + setStatusBarColor(R.color.backgroundLightGray) store.subscribe(subscriber = this) { state -> state .skipRepeats { oldState, newState -> oldState.disclaimerState == newState.disclaimerState } @@ -65,4 +68,8 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs webView.loadUrl(state.type.uri.toString()) } + + override fun handleOnBackPressed() { + store.dispatch(DisclaimerAction.OnBackPressed) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index c879e6ca04..0c5b990b06 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -79,13 +79,17 @@ private fun readCard() = scope.launch { onProgressStateChange = { showProgress -> if (showProgress) { changeButtonState(ButtonState.PROGRESS) - } else { - changeButtonState(ButtonState.ENABLED) } + // else { //todo hide this because + // changeButtonState(ButtonState.ENABLED) + // } }, onScanStateChange = { scanInProgress -> store.dispatch(HomeAction.ScanInProgress(scanInProgress)) }, + onFailure = { + changeButtonState(ButtonState.ENABLED) + }, onSuccess = { scanResponse -> scope.launch { if (preferencesStorage.shouldSaveUserWallets) { @@ -93,21 +97,18 @@ private fun readCard() = scope.launch { userWalletsListManager.save(userWallet) .doOnFailure { error -> Timber.e(error, "Unable to save user wallet") - tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false) store.onCardScanned(scanResponse) } .doOnSuccess { - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes && - userWallet.hasAccessCode, - ) - store.onUserWalletSelected(userWallet) + scope.launch { store.onUserWalletSelected(userWallet) } } .doOnResult { + changeButtonState(ButtonState.ENABLED) store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) } } else { store.onCardScanned(scanResponse) + changeButtonState(ButtonState.ENABLED) store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index b1ce166222..9b5c5e9958 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -1,20 +1,16 @@ package com.tangem.tap.features.onboarding -import com.tangem.common.doOnSuccess import com.tangem.domain.common.ProductType import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.onCardScanned -import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.features.saveWallet.redux.SaveWalletAction import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager -import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -59,17 +55,6 @@ class OnboardingHelper { backupCardsIds: List? = null, ) { when { - // When should save user wallets but manager is locked, then unlock manager with card - preferencesStorage.shouldSaveUserWallets && - userWalletsListManager.isLockedSync -> scope.launch { - val userWallet = UserWalletBuilder(scanResponse).build() - - tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false) - userWalletsListManager.unlockWithCard(userWallet) - .doOnSuccess { - store.onUserWalletSelected(userWallet) - } - } // When should save user wallets, then save card without navigate to save wallet screen preferencesStorage.shouldSaveUserWallets -> scope.launch { store.dispatchOnMain( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index 8853e2766e..ae60d9f637 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -6,6 +6,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.extensions.guard import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.extensions.withMainContext +import com.tangem.domain.common.util.userWalletId import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.events.AnalyticsParam @@ -31,6 +32,7 @@ import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store +import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action @@ -69,7 +71,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { fun updateScanResponse(response: ScanResponse) { when (twinCardsState.mode) { CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse = response - CreateTwinWalletMode.RecreateWallet -> store.dispatch(GlobalAction.SaveScanNoteResponse(response)) + CreateTwinWalletMode.RecreateWallet -> store.dispatch(GlobalAction.SaveScanResponse(response)) } } @@ -151,10 +153,15 @@ private fun handle(action: Action, dispatch: DispatchFunction) { finishCardActivation() postUi(500) { store.dispatch(TwinCardsAction.Confetti.Show) } } - + TwinCardsStep.CreateFirstWallet -> { + scope.launch { + userWalletsListManager.delete( + listOf(getScanResponse().card.userWalletId), + ) + } + } TwinCardsStep.None, TwinCardsStep.Warning, - TwinCardsStep.CreateFirstWallet, TwinCardsStep.CreateSecondWallet, TwinCardsStep.CreateThirdWallet, -> Unit @@ -294,13 +301,14 @@ private fun handle(action: Action, dispatch: DispatchFunction) { when (twinCardsState.mode) { CreateTwinWalletMode.CreateWallet -> { store.dispatchOnMain(GlobalAction.Onboarding.Stop) - OnboardingHelper.trySaveWalletAndNavigateToWalletScreen( - scanResponse = scanResponse, - backupCardsIds = listOfNotNull(twinCardsState.twinCardsManager?.secondCardPublicKey), - ) + OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) } CreateTwinWalletMode.RecreateWallet -> { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) + if (preferencesStorage.shouldSaveUserWallets) { + OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) + } else { + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) + } } } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index 9511376155..36e386325e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -13,6 +13,7 @@ import coil.load import com.tangem.Message import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.VoidCallback +import com.tangem.core.ui.fragments.setStatusBarColor import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TwinCardNumber import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget @@ -97,6 +98,11 @@ class TwinsCardsFragment : BaseOnboardingFragment() { } } + override fun onStart() { + super.onStart() + setStatusBarColor(R.color.backgroundWhite) + } + private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) = with(containerBinding) { imvFrontCard.hide() 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 d11d5139fb..106ebfa9af 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 @@ -94,7 +94,6 @@ internal class SaveWalletMiddleware { saveAccessCodeIfNeeded(state.backupInfo?.accessCode, userWallet.cardsInWallet) .flatMap { userWalletsListManager.save(userWallet, canOverride = true) } - .flatMap { userWalletsListManager.selectWallet(userWallet.walletId) } .doOnFailure { error -> store.dispatchOnMain(SaveWalletAction.Save.Error(error)) } @@ -105,12 +104,6 @@ internal class SaveWalletMiddleware { preferencesStorage.shouldSaveAccessCodes = isFirstSavedWallet || preferencesStorage.shouldSaveAccessCodes - - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes && - userWallet.hasAccessCode, - ) - store.dispatchOnMain(SaveWalletAction.Save.Success) store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index b093418152..beeb35f0f6 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -48,6 +48,7 @@ import com.tangem.tap.features.send.redux.states.ButtonState import com.tangem.tap.features.send.redux.states.ExternalTransactionData import com.tangem.tap.features.send.redux.states.MainCurrencyType import com.tangem.tap.features.send.redux.states.TransactionExtrasState +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.scope import com.tangem.tap.store @@ -328,12 +329,15 @@ private fun updateWarnings(dispatch: (Action) -> Unit) { } private suspend fun updateWallet(walletManager: WalletManager) { - val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager) val selectedUserWallet = userWalletsListManager.selectedUserWalletSync if (selectedUserWallet != null) { + val wallet = walletManager.wallet walletCurrenciesManager.update( userWallet = selectedUserWallet, - blockchainNetwork = blockchainNetwork, + currency = Currency.Blockchain( + blockchain = wallet.blockchain, + derivationPath = wallet.publicKey.derivationPath?.rawPath, + ), ) } else { store.dispatchOnMain(WalletAction.LoadWallet(BlockchainNetwork.fromWalletManager(walletManager))) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index 8465e9d228..b07a4fd3ca 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -221,7 +221,7 @@ class TokensMiddleware { val updatedScanResponse = scanResponse.copy( derivedKeys = updatedDerivedKeys, ) - store.dispatchOnMain(GlobalAction.SaveScanNoteResponse(updatedScanResponse)) + store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse)) delay(DELAY_SDK_DIALOG_CLOSE) onSuccess(updatedScanResponse) @@ -383,7 +383,7 @@ class TokensMiddleware { } } - val addedCurrencies = store.state.walletState.wallets.map { walletStore -> + val addedCurrencies = store.state.walletState.walletsStores.map { walletStore -> walletStore.walletsData.map { walletData -> walletData.currency } }.flatten().map { when (it) { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt index ea9a272f74..2fa1ab2971 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt @@ -57,7 +57,7 @@ class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubsc super.onViewCreated(view, savedInstanceState) (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) - toolbar.setNavigationOnClickListener { activity?.onBackPressed() } + toolbar.setNavigationOnClickListener { handleOnBackPressed() } val onSaveChanges = { tokens: List, blockchains: List -> Analytics.send(ManageTokens.ButtonSaveChanges()) @@ -144,7 +144,6 @@ class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubsc } override fun handleOnBackPressed() { - super.handleOnBackPressed() store.dispatch(NavigationAction.PopBackTo()) store.dispatch(TokensAction.ResetState) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt index 6d751e8956..149fd2b517 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.tangem.blockchain.common.Blockchain +import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.tap.common.analytics.Analytics @@ -81,6 +82,11 @@ fun CurrenciesScreen( } } + val statusBarColor = colorResource(id = R.color.backgroundLightGray) + SystemBarsEffect { + setSystemBarsColor(color = statusBarColor) + } + Scaffold( floatingActionButton = { if (tokensState.value.allowToAdd) { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index be432defdc..0cc8039e96 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -12,11 +12,11 @@ import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.warningMessage.WarningMessage -import com.tangem.tap.domain.model.TotalFiatBalance import com.tangem.tap.domain.model.UserWallet import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.features.wallet.models.TotalBalance import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.wallet.R import org.rekotlin.Action @@ -36,7 +36,7 @@ sealed class WalletAction : Action { data class LoadWallet( val blockchain: BlockchainNetwork? = null, - val walletManager: WalletManager? = null + val walletManager: WalletManager? = null, ) : WalletAction() { data class Success(val wallet: Wallet, val blockchain: BlockchainNetwork) : WalletAction() data class NoAccount( @@ -90,7 +90,8 @@ sealed class WalletAction : Action { val blockchain: BlockchainNetwork, ) : MultiWallet() - data class SelectWallet(val walletData: WalletData?) : MultiWallet() + data class SelectWallet(val currency: Currency?) : MultiWallet() + data class SetSingleWalletCurrency(val currency: Currency?) : MultiWallet() data class TryToRemoveWallet(val currency: Currency) : MultiWallet() data class RemoveWallet(val currency: Currency) : MultiWallet() @@ -128,7 +129,7 @@ sealed class WalletAction : Action { val wallet: Wallet? = null, val coinsList: List? = null, ) : WalletAction() { data class Success( - val fiatRates: Map + val fiatRates: Map, ) : WalletAction() object Failure : WalletAction() @@ -163,7 +164,7 @@ sealed class WalletAction : Action { object ChooseTradeActionDialog : DialogAction() data class ChooseCurrency(val amounts: List?) : DialogAction() data class RussianCardholdersWarningDialog( - val dialogData: WalletDialog.RussianCardholdersWarningDialog.Data? = null + val dialogData: WalletDialog.RussianCardholdersWarningDialog.Data? = null, ) : DialogAction() object Hide : DialogAction() @@ -181,12 +182,13 @@ sealed class WalletAction : Action { data class Buy( val checkUserLocation: Boolean = true, ) : TradeCryptoAction() + data class FinishSelling(val transactionId: String) : TradeCryptoAction() data class SendCrypto( val currencyId: String, val amount: String, val destinationAddress: String, - val transactionId: String + val transactionId: String, ) : TradeCryptoAction() } @@ -206,6 +208,9 @@ sealed class WalletAction : Action { } data class UserWalletChanged(val userWallet: UserWallet) : WalletAction() - data class WalletStoresChanged(val walletStores: List) : WalletAction() - data class TotalFiatBalanceChanged(val balance: TotalFiatBalance) : WalletAction() + data class WalletStoresChanged(val walletStores: List) : WalletAction() { + data class UpdateWalletStores(val reduxWalletStores: List) : WalletAction() + } + + data class TotalFiatBalanceChanged(val balance: TotalBalance) : WalletAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index b8413753cb..49773a9854 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -36,7 +36,7 @@ data class WalletState( val cardImage: Artwork? = null, val hashesCountVerified: Boolean? = null, val mainWarningsList: List = mutableListOf(), - val wallets: List = listOf(), + val walletsStores: List = listOf(), val isMultiwalletAllowed: Boolean = false, val cardCurrency: CryptoCurrencyName? = null, val selectedCurrency: Currency? = null, @@ -50,6 +50,12 @@ data class WalletState( val walletCardsCount: Int? = null, ) : StateType { + val walletsDataFromStores: List + get() = walletsStores.map { it.walletsData }.flatten() + + val selectedWalletData: WalletData? + get() = walletsDataFromStores.firstOrNull { it.currency == selectedCurrency } + // if you do not delegate - the application crashes on startup, // because twinCardsState has not been created yet val twinCardsState: TwinCardsState by ReadOnlyProperty { _, _ -> @@ -63,20 +69,17 @@ data class WalletState( get() = store.state.globalState.exchangeManager.featureIsSwitchedOn() val blockchains: List - get() = wallets.mapNotNull { it.walletManager?.wallet?.blockchain } + get() = walletsStores.mapNotNull { it.walletManager?.wallet?.blockchain } val currencies: List - get() = wallets.flatMap { it.walletsData }.map { it.currency } - - val walletsData: List - get() = wallets.flatMap { it.walletsData } + get() = walletsStores.flatMap { it.walletsData }.map { it.currency } val walletManagers: List - get() = wallets.mapNotNull { it.walletManager } + get() = walletsStores.mapNotNull { it.walletManager } - val primaryWallet: WalletData? = wallets.firstOrNull()?.walletsData?.firstOrNull() + val primaryWallet: WalletData? = walletsStores.firstOrNull()?.walletsData?.firstOrNull() - val primaryWalletManager: WalletManager? = if (wallets.isNotEmpty()) wallets[0].walletManager else null + val primaryWalletManager: WalletManager? = if (walletsStores.isNotEmpty()) walletsStores[0].walletManager else null val shouldShowDetails: Boolean = primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard && @@ -91,12 +94,12 @@ data class WalletState( } fun getWalletManager(blockchain: BlockchainNetwork): WalletManager? { - return wallets.find { it.blockchainNetwork == blockchain }?.walletManager + return walletsStores.find { it.blockchainNetwork == blockchain }?.walletManager } fun getWalletData(blockchain: BlockchainNetwork?): WalletData? { if (blockchain == null) return null - return walletsData.find { + return walletsDataFromStores.find { it.currency is Currency.Blockchain && it.currency.blockchain == blockchain.blockchain && it.currency.derivationPath == blockchain.derivationPath @@ -105,7 +108,7 @@ data class WalletState( fun getWalletStore(currency: Currency?): WalletStore? { if (currency == null) return null - return wallets.firstOrNull { + return walletsStores.firstOrNull { it.blockchainNetwork.derivationPath == currency.derivationPath && (it.blockchainNetwork.blockchain == currency.blockchain) } @@ -120,7 +123,7 @@ data class WalletState( fun getWalletStore(blockchainNetwork: BlockchainNetwork?): WalletStore? { if (blockchainNetwork == null) return null - return wallets.firstOrNull { + return walletsStores.firstOrNull { it.blockchainNetwork.derivationPath == blockchainNetwork.derivationPath && (it.blockchainNetwork.blockchain == blockchainNetwork.blockchain) } @@ -131,10 +134,6 @@ data class WalletState( return getWalletStore(currency)?.walletsData?.firstOrNull { it.currency == currency } } - fun getSelectedWalletData(): WalletData? { - return walletsData.find { it.currency == selectedCurrency } - } - private fun isPrimaryCurrency(walletData: WalletData): Boolean { return (walletData.currency is Currency.Blockchain && walletData.currency.blockchain == store.state.walletState.primaryBlockchain) @@ -142,10 +141,10 @@ data class WalletState( walletData.currency.token == store.state.walletState.primaryToken) } - fun replaceWalletInWallets(wallet: WalletStore?): List { - if (wallet == null) return wallets + fun replaceWalletStoreInWalletsStores(wallet: WalletStore?): List { + if (wallet == null) return walletsStores var changed = false - val updatedWallets = wallets.map { + val updatedWallets = walletsStores.map { if (it.blockchainNetwork == wallet.blockchainNetwork) { changed = true wallet @@ -153,7 +152,7 @@ data class WalletState( it } } - return if (changed) updatedWallets else wallets + wallet + return if (changed) updatedWallets else walletsStores + wallet } fun updateWalletData(walletData: WalletData?): WalletState { @@ -161,26 +160,23 @@ data class WalletState( return updateWalletsData(listOf(walletData)) } - fun updateWalletsData( - walletsData: List - ): WalletState { - + fun updateWalletsData(walletsData: List): WalletState { val walletStores = walletsData .map { BlockchainNetwork(it.currency.blockchain, it.currency.derivationPath, emptyList()) } .distinct().map { getWalletStore(it) }.mapNotNull { it?.updateWallets(walletsData) } - return updateWalletStores(walletStores) + return updateWalletsStores(walletStores) } fun updateWalletStore(walletStore: WalletStore?): WalletState { - return copy(wallets = replaceWalletInWallets(walletStore)) + return copy(walletsStores = replaceWalletStoreInWalletsStores(walletStore)) .updateTotalBalance() .updateProgressState() } - private fun updateWalletStores(walletStores: List): WalletState { + private fun updateWalletsStores(walletStores: List): WalletState { val walletStoresMutable = walletStores.toMutableList() - val updatedWallets = wallets.map { oldWalletStore -> + val updatedWallets = walletsStores.map { oldWalletStore -> val walletStore = walletStoresMutable.find { it.blockchainNetwork == oldWalletStore.blockchainNetwork } @@ -191,20 +187,20 @@ data class WalletState( oldWalletStore } } - return copy(wallets = updatedWallets + walletStoresMutable) + return copy(walletsStores = updatedWallets + walletStoresMutable) .updateTotalBalance() .updateProgressState() } - fun removeWallet(walletData: WalletData?): WalletState { + fun removeWalletData(walletData: WalletData?): WalletState { if (walletData == null) return this return when (val currency = walletData.currency) { is Currency.Blockchain -> { - val walletStores = wallets.filterNot { + val walletStores = walletsStores.filterNot { it.blockchainNetwork.blockchain == currency.blockchain && it.blockchainNetwork.derivationPath == currency.derivationPath } - copy(wallets = walletStores) + copy(walletsStores = walletStores) .updateTotalBalance() .updateProgressState() } @@ -223,23 +219,8 @@ data class WalletState( } } - fun replaceSomeWallets(newWallets: List): List { - val remainingWallets: MutableList = newWallets.toMutableList() - val updatedWallets = walletsData.map { wallet -> - val newWallet = newWallets - .firstOrNull { wallet.currency == it.currency } - if (newWallet == null) { - wallet - } else { - remainingWallets.remove(newWallet) - newWallet - } - } - return updatedWallets + remainingWallets - } - private fun updateTotalBalance(): WalletState { - val walletsData = this.wallets + val walletsData = this.walletsStores .flatMap(WalletStore::walletsData) return if (walletsData.isNotEmpty()) { @@ -256,7 +237,7 @@ data class WalletState( } private fun updateProgressState(): WalletState { - val walletsData = this.wallets + val walletsData = this.walletsStores .flatMap(WalletStore::walletsData) return if (walletsData.isNotEmpty()) { @@ -264,7 +245,7 @@ data class WalletState( this.copy( state = walletsData.findProgressState(), - error = this.error.takeIf { newProgressState == ProgressState.Error } + error = this.error.takeIf { newProgressState == ProgressState.Error }, ) } else this } @@ -276,6 +257,21 @@ data class WalletState( } } +fun List.replaceSomeWalletsData(newWallets: List): List { + val remainingWallets: MutableList = newWallets.toMutableList() + val updatedWallets = this.map { wallet -> + val newWallet = newWallets + .firstOrNull { wallet.currency == it.currency } + if (newWallet == null) { + wallet + } else { + remainingWallets.remove(newWallet) + newWallet + } + } + return updatedWallets + remainingWallets +} + enum class ProgressState : WidgetState { Loading, Refreshing, Done, Error } enum class ErrorType { NoInternetConnection } @@ -287,7 +283,7 @@ sealed class WalletMainButton(enabled: Boolean) : Button(enabled) { data class WalletAddresses( val selectedAddress: AddressData, - val list: List + val list: List, ) data class AddressData( @@ -301,7 +297,7 @@ data class AddressData( data class Artwork( val artworkId: String, - val artwork: Bitmap? = null + val artwork: Bitmap? = null, ) { companion object { const val DEFAULT_IMG_URL = "https://app.tangem.com/cards/card_default.png" @@ -374,7 +370,7 @@ data class WalletData( } } - private fun assembleTokenWarnings(walletWarnings: MutableList){ + private fun assembleTokenWarnings(walletWarnings: MutableList) { if (!currency.isToken()) return val blockchainFullName = currency.blockchain.fullName @@ -391,7 +387,7 @@ data class WalletData( data class WalletStore( val walletManager: WalletManager?, val blockchainNetwork: BlockchainNetwork, - val walletsData: List + val walletsData: List, ) { fun updateWallets(walletDataList: List): WalletStore { val relevantWalletDataList = walletDataList.filter { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/Mapper.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/Mapper.kt new file mode 100644 index 0000000000..dbb030f67f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/Mapper.kt @@ -0,0 +1,133 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import com.tangem.common.extensions.isZero +import com.tangem.tap.common.extensions.stripZeroPlainString +import com.tangem.tap.common.extensions.toFiatRateString +import com.tangem.tap.common.extensions.toFiatValue +import com.tangem.tap.common.extensions.toFormattedCurrencyString +import com.tangem.tap.common.extensions.toFormattedFiatValue +import com.tangem.tap.domain.model.TotalFiatBalance +import com.tangem.tap.domain.model.WalletDataModel +import com.tangem.tap.domain.model.WalletStoreModel +import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.features.wallet.models.TotalBalance +import com.tangem.tap.features.wallet.models.WalletRent +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.features.wallet.redux.WalletAddresses +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletMainButton +import com.tangem.tap.features.wallet.redux.WalletStore +import com.tangem.tap.features.wallet.ui.BalanceStatus +import com.tangem.tap.features.wallet.ui.BalanceWidgetData +import com.tangem.tap.features.wallet.ui.TokenData +import com.tangem.tap.store + +internal fun List.mapToReduxModels( + isMultiWalletAllowed: Boolean, +): List { + return this.map { walletStoreModel -> + walletStoreModel.mapToReduxModel(isMultiWalletAllowed) + } +} + +internal fun TotalFiatBalance.mapToReduxModel(): TotalBalance { + return TotalBalance( + state = when (this) { + is TotalFiatBalance.Loading -> ProgressState.Loading + is TotalFiatBalance.Error -> ProgressState.Error + is TotalFiatBalance.Loaded -> ProgressState.Done + }, + fiatAmount = amount, + fiatCurrency = store.state.globalState.appCurrency, + ) +} + +internal fun WalletStoreModel.mapToReduxModel( + isMultiWalletAllowed: Boolean, +): WalletStore { + return WalletStore( + walletManager = walletManager, + blockchainNetwork = blockchainNetwork, + walletsData = walletsData.mapToReduxModel(isMultiWalletAllowed, walletRent), + ) +} + +private fun List.mapToReduxModel( + isMultiWalletAllowed: Boolean, + walletRent: WalletStoreModel.WalletRent?, +): List { + return this.map { walletDataModel -> + with(walletDataModel) { + val amount = status.amount + val amountFormatted = amount.toFormattedCurrencyString( + decimals = currency.decimals, + currency = currency.currencySymbol, + ) + val appCurrency = store.state.globalState.appCurrency + val fiatAmount = fiatRate?.let { status.amount.toFiatValue(it) } + val fiatAmountFormatted = fiatAmount + ?.takeIf { !status.isErrorStatus } + ?.toFormattedFiatValue(appCurrency.symbol) + val fiatRateFormatted = fiatRate?.toFiatRateString(appCurrency.symbol) + + WalletData( + currency = currency, + walletAddresses = walletAddresses.getOrNull(0)?.let { selectedAddress -> + WalletAddresses( + selectedAddress = selectedAddress, + list = walletAddresses, + ) + }, + existentialDepositString = existentialDeposit?.toPlainString(), + fiatRate = fiatRate, + fiatRateString = fiatRateFormatted, + pendingTransactions = status.pendingTransactions, + mainButton = WalletMainButton.SendButton( + enabled = !status.amount.isZero() && status.pendingTransactions.isEmpty(), + ), + walletRent = walletRent?.let { + WalletRent( + minRentValue = "${it.rent.stripZeroPlainString()} ${currency.blockchain.currency}", + rentExemptValue = "${it.exemptionAmount.stripZeroPlainString()} ${currency.blockchain.currency}", + ) + }, + currencyData = BalanceWidgetData( + status = when (status) { + is WalletDataModel.Loading -> BalanceStatus.Loading + is WalletDataModel.NoAccount -> BalanceStatus.NoAccount + is WalletDataModel.SameCurrencyTransactionInProgress -> BalanceStatus.SameCurrencyTransactionInProgress + is WalletDataModel.TransactionInProgress -> BalanceStatus.TransactionInProgress + is WalletDataModel.Unreachable -> BalanceStatus.Unreachable + is WalletDataModel.MissedDerivation -> BalanceStatus.MissedDerivation + is WalletDataModel.VerifiedOnline -> BalanceStatus.VerifiedOnline + }, + currency = currency.currencyName, + currencySymbol = currency.currencySymbol, + blockchainAmount = status.amount, + amount = amount, + amountFormatted = amountFormatted, + fiatAmount = fiatAmount, + fiatAmountFormatted = fiatAmountFormatted, + token = when { + !isMultiWalletAllowed && currency is Currency.Token -> { + TokenData( + amount = amount, + amountFormatted = amountFormatted, + fiatAmount = fiatAmount, + fiatAmountFormatted = fiatAmountFormatted, + tokenSymbol = currency.currencySymbol, + fiatRate = fiatRate, + fiatRateString = fiatRateFormatted, + ) + } + else -> null + }, + amountToCreateAccount = (status as? WalletDataModel.NoAccount) + ?.amountToCreateAccount + ?.toString(), + errorMessage = status.errorMessage, + ), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index bbe6f90378..9bba0011a5 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -51,7 +51,7 @@ class MultiWalletMiddleware { handleAddingWalletManagers(globalState, action.walletManagers) } is WalletAction.MultiWallet.SelectWallet -> { - if (action.walletData != null) { + if (action.currency != null) { store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletDetails)) } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 5d6a9fc2d7..df69426a2c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -44,7 +44,7 @@ class TradeCryptoMiddleware { return } - val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return + val selectedWalletData = store.state.walletState.selectedWalletData ?: return val card = store.state.globalState.scanResponse?.card ?: return val addresses = selectedWalletData.walletAddresses?.list.orEmpty() @@ -85,7 +85,7 @@ class TradeCryptoMiddleware { } private fun proceedSellAction() { - val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return + val selectedWalletData = store.state.walletState.selectedWalletData ?: return val appCurrency = store.state.globalState.appCurrency val addresses = selectedWalletData.walletAddresses?.list.orEmpty() @@ -107,7 +107,7 @@ class TradeCryptoMiddleware { } private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) { - val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return + val selectedWalletData = store.state.walletState.selectedWalletData ?: return Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(selectedWalletData.currency))) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt index f94e6def86..fb6ff1dd3f 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt @@ -17,7 +17,7 @@ class WalletDialogsMiddleware { store.dispatchDialogShow(WalletDialog.SignedHashesMultiWalletDialog) } is WalletAction.DialogAction.ChooseTradeActionDialog -> { - store.state.walletState.getSelectedWalletData()?.let { + store.state.walletState.selectedWalletData?.let { Analytics.send(Token.ButtonExchange(AnalyticsParam.CurrencyType.Currency(it.currency))) } store.dispatchDialogShow(WalletDialog.ChooseTradeActionDialog) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index f6e0c47011..5dcd8b81bd 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -97,11 +97,7 @@ class WalletMiddleware { when (action) { is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(state, action) is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState) - is WalletAction.MultiWallet -> multiWalletMiddleware.handle( - action, - walletState, - globalState - ) + is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState, globalState) is WalletAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action) is WalletAction.DialogAction -> walletDialogMiddleware.handle(action) is WalletAction.LoadWallet -> { @@ -129,9 +125,7 @@ class WalletMiddleware { true, ), ) - store.dispatch( - action = WalletAction.LoadWallet.Success(action.wallet, action.blockchain) - ) + store.dispatch(WalletAction.LoadWallet.Success(action.wallet, action.blockchain)) } } store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline) @@ -150,7 +144,7 @@ class WalletMiddleware { action.coinsList != null -> action.coinsList else -> { if (walletState.isMultiwalletAllowed) { - walletState.walletsData.map { it.currency } + walletState.walletsDataFromStores.map { it.currency } } else { val derivationPath = walletState.primaryWallet?.currency?.derivationPath val primaryBlockchain = walletState.primaryBlockchain @@ -175,7 +169,7 @@ class WalletMiddleware { Timber.e( throwable, "Loading rates failed for [%s]", - currency.currencySymbol + currency.currencySymbol, ) } } @@ -188,14 +182,11 @@ class WalletMiddleware { } is WalletAction.CreateWallet -> { scope.launch { - val result = tangemSdkManager.createWallet( - globalState.scanResponse?.card?.cardId - ) + val result = tangemSdkManager.createWallet(globalState.scanResponse?.card?.cardId) when (result) { is CompletionResult.Success -> { - val scanNoteResponse = - globalState.scanResponse?.copy(card = result.data) - scanNoteResponse?.let { store.onCardScanned(scanNoteResponse) } + val scanResponse = globalState.scanResponse?.copy(card = result.data) + scanResponse?.let { store.onCardScanned(scanResponse) } } is CompletionResult.Failure -> {} } @@ -210,9 +201,7 @@ class WalletMiddleware { store.dispatchOnMain(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed)) scope.launch { - val response = OnlineCardVerifier().getCardInfo( - action.card.cardId, action.card.cardPublicKey - ) + val response = OnlineCardVerifier().getCardInfo(action.card.cardId, action.card.cardPublicKey) when (response) { is Result.Success -> { val actionList = listOf( @@ -237,11 +226,12 @@ class WalletMiddleware { refresh = action is WalletAction.LoadData.Refresh, ) } else { - val scanNoteResponse = globalState.scanResponse ?: return@launch - if (walletState.walletsData.isNotEmpty()) { - globalState.tapWalletManager.reloadData(scanNoteResponse) + val scanResponse = globalState.scanResponse ?: return@launch + + if (walletState.walletsDataFromStores.isNotEmpty()) { + globalState.tapWalletManager.reloadData(scanResponse) } else { - globalState.tapWalletManager.loadData(scanNoteResponse) + globalState.tapWalletManager.loadData(scanResponse) } } } @@ -249,8 +239,8 @@ class WalletMiddleware { is NetworkStateChanged -> { store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline) val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync - if (selectedUserWallet != null) scope.launch { - globalState.tapWalletManager.loadData(selectedUserWallet) + if (selectedUserWallet != null) { + scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet) } } else { globalState.scanResponse?.let { scanNoteResponse -> scope.launch { globalState.tapWalletManager.loadData(scanNoteResponse) } @@ -295,6 +285,7 @@ class WalletMiddleware { } is WalletAction.UserWalletChanged -> Unit is WalletAction.WalletStoresChanged -> { + updateWalletStores(action.walletStores, walletState) fetchTotalFiatBalance(action.walletStores, walletState) findMissedDerivations(action.walletStores) tryToShowAppRatingWarning(action.walletStores) @@ -303,12 +294,29 @@ class WalletMiddleware { } } + private fun updateWalletStores(wallStores: List, state: WalletState) { + scope.launch(Dispatchers.Default) { + if (!state.isMultiwalletAllowed) { + wallStores.firstOrNull()?.walletsData?.firstOrNull()?.let { + store.dispatchOnMain(WalletAction.MultiWallet.SetSingleWalletCurrency(it.currency)) + } + } + + val reduxWalletStores = wallStores.mapToReduxModels(state.isMultiwalletAllowed) + store.dispatchOnMain( + WalletAction.WalletStoresChanged.UpdateWalletStores( + reduxWalletStores = reduxWalletStores.toList(), + ), + ) + } + } + private fun fetchTotalFiatBalance(walletStores: List, state: WalletState) { scope.launch(Dispatchers.Default) { val totalFiatBalance = totalFiatBalanceCalculator.calculateOrNull( prevAmount = state.totalBalance?.fiatAmount, walletStores = walletStores, - ) + )?.mapToReduxModel() if (totalFiatBalance != null) { store.dispatchOnMain(WalletAction.TotalFiatBalanceChanged(totalFiatBalance)) @@ -361,7 +369,7 @@ class WalletMiddleware { } private fun prepareSendAction(amount: Amount?, state: WalletState?): Action { - val selectedWalletData = state?.getSelectedWalletData() + val selectedWalletData = state?.selectedWalletData val currency = selectedWalletData?.currency val walletStore = state?.getWalletStore(currency) @@ -376,36 +384,34 @@ class WalletMiddleware { if (currency != null && state.isMultiwalletAllowed) { when (currency) { is Currency.Blockchain -> { - val amountToSend = - amounts?.find { it.currencySymbol == currency.blockchain.currency } - ?: return WalletAction.DialogAction.ChooseCurrency(amounts) + val amountToSend = amounts?.find { it.currencySymbol == currency.blockchain.currency } + ?: return WalletAction.DialogAction.ChooseCurrency(amounts) PrepareSendScreen( coinAmount = amountToSend, coinRate = selectedWalletData.fiatRate, - walletManager = walletStore.walletManager + walletManager = walletStore.walletManager, ) } is Currency.Token -> { - val amountToSend = - amounts?.find { it.currencySymbol == currency.token.symbol } - ?: return WalletAction.DialogAction.ChooseCurrency(amounts) + val amountToSend = amounts?.find { it.currencySymbol == currency.token.symbol } + ?: return WalletAction.DialogAction.ChooseCurrency(amounts) prepareSendActionForToken( amount = amountToSend, state = state, selectedWalletData = selectedWalletData, - walletStore = walletStore + walletStore = walletStore, ) } } } else { - if (amounts?.size ?: 0 > 1) { + if ((amounts?.size ?: 0) > 1) { WalletAction.DialogAction.ChooseCurrency(amounts) } else { val amountToSend = amounts?.first() PrepareSendScreen( coinAmount = amountToSend, coinRate = selectedWalletData?.fiatRate, - walletManager = walletStore?.walletManager + walletManager = walletStore?.walletManager, ) } } @@ -416,7 +422,7 @@ class WalletMiddleware { amount: Amount, state: WalletState?, selectedWalletData: WalletData?, - walletStore: WalletStore? + walletStore: WalletStore?, ): PrepareSendScreen { val coinRate = state?.getWalletData(walletStore?.blockchainNetwork)?.fiatRate val tokenRate = if (state?.isMultiwalletAllowed == true) { @@ -431,7 +437,7 @@ class WalletMiddleware { coinRate = coinRate, walletManager = walletStore?.walletManager, tokenAmount = amount, - tokenRate = tokenRate + tokenRate = tokenRate, ) } @@ -447,7 +453,7 @@ class WalletMiddleware { val balance = walletManager.wallet.fundsAvailable(AmountType.Coin) val outgoingTxs = walletManager.wallet.getPendingTransactions( - PendingTransactionType.Outgoing + PendingTransactionType.Outgoing, ).filterByCoin() val rentExempt = result.data @@ -465,8 +471,8 @@ class WalletMiddleware { WalletAction.SetWalletRent( wallet = walletManager.wallet, minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"), - rentExempt = ("${rentExempt.stripZeroPlainString()} $currency") - ) + rentExempt = ("${rentExempt.stripZeroPlainString()} $currency"), + ), ) } else { dispatchOnMain(WalletAction.RemoveWalletRent(walletManager.wallet)) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt index e911c48e3c..b2ef29f6b3 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -125,7 +125,8 @@ class WarningsMiddleware { if (scanResponse.isTangemTwins() || scanResponse.isDemoCard()) return null if (scanResponse.card.isMultiwalletAllowed) { - return if (scanResponse.card.hasSignedHashes()) { + val isBackupForbidden = with(scanResponse.card.settings) { !(isBackupAllowed || isHDWalletAllowed) } + return if (scanResponse.card.hasSignedHashes() && isBackupForbidden) { WarningMessagesManager.signedHashesMultiWalletWarning() } else { store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId) @@ -133,8 +134,7 @@ class WarningsMiddleware { } } - val validator = store.state.walletState.walletManagers.firstOrNull() - as? SignatureCountValidator + val validator = store.state.walletState.walletManagers.firstOrNull() as? SignatureCountValidator return if (validator == null) { if (scanResponse.card.hasSignedHashes()) { WarningMessagesManager.alreadySignedHashesWarning() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt index 6885dbc11d..729d99f960 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt @@ -36,7 +36,7 @@ class MultiWalletReducer { fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState { return when (action) { is WalletAction.MultiWallet.AddBlockchains -> { - val wallets: List = action.blockchains.map { blockchain -> + val walletStores: List = action.blockchains.mapNotNull { blockchain -> val walletManager = action.walletManagers.firstOrNull { it.wallet.blockchain == blockchain.blockchain && (it.wallet.publicKey.derivationPath?.rawPath == blockchain.derivationPath) @@ -52,13 +52,13 @@ class MultiWalletReducer { status = BalanceStatus.Loading, currency = blockchain.blockchain.fullName, currencySymbol = blockchain.blockchain.currency, - token = cardToken + token = cardToken, ), walletAddresses = createAddressList(wallet), mainButton = WalletMainButton.SendButton(false), currency = Currency.Blockchain( blockchain.blockchain, - blockchain.derivationPath + blockchain.derivationPath, ), existentialDepositString = getExistentialDeposit(walletManager), ) @@ -66,16 +66,19 @@ class MultiWalletReducer { WalletStore( walletManager = walletManager, blockchainNetwork = blockchain, - walletsData = listOf(walletData) + walletsData = listOf(walletData), ) } val selectedCurrency = if (!state.isMultiwalletAllowed) { - wallets.firstOrNull()?.walletsData?.firstOrNull()?.currency + walletStores.firstOrNull()?.walletsData?.firstOrNull()?.currency } else { state.selectedCurrency } - state.copy(wallets = wallets, selectedCurrency = selectedCurrency) + state.copy( + walletsStores = walletStores, + selectedCurrency = selectedCurrency, + ) } is WalletAction.MultiWallet.AddBlockchain -> { val walletManager = action.walletManager ?: state.getWalletManager(action.blockchain) @@ -91,14 +94,14 @@ class MultiWalletReducer { mainButton = WalletMainButton.SendButton(false), currency = Currency.Blockchain( action.blockchain.blockchain, - action.blockchain.derivationPath + action.blockchain.derivationPath, ), existentialDepositString = getExistentialDeposit(walletManager), ) val walletStore = WalletStore( walletManager = walletManager, blockchainNetwork = action.blockchain, - walletsData = listOf(walletData) + walletsData = listOf(walletData), ) val newState = state.updateWalletStore(walletStore) @@ -142,31 +145,40 @@ class MultiWalletReducer { status = tokenBalanceStatus, amount = action.amount.value, amountFormatted = action.amount.value?.toFormattedCurrencyString( - action.amount.decimals, action.amount.currencySymbol + action.amount.decimals, action.amount.currencySymbol, ), fiatAmountFormatted = tokenWalletData.fiatRate?.let { action.amount.value?.toFiatString(it, store.state.globalState.appCurrency.symbol) } ?: UNKNOWN_AMOUNT_SIGN, - blockchainAmount = wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO + blockchainAmount = wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO, ), pendingTransactions = pendingTransactions.removeUnknownTransactions(), mainButton = WalletMainButton.SendButton(isTokenSendButtonEnabled), currency = Currency.Token( token = action.token, blockchain = action.blockchain.blockchain, - derivationPath = action.blockchain.derivationPath + derivationPath = action.blockchain.derivationPath, ), - walletRent = findWalletRent(state.getWalletStore(walletManager.wallet)) + walletRent = findWalletRent(state.getWalletStore(walletManager.wallet)), ) state.updateWalletData(newTokenWalletData) } - is WalletAction.MultiWallet.SetIsMultiwalletAllowed -> state.copy(isMultiwalletAllowed = action.isMultiwalletAllowed) - is WalletAction.MultiWallet.SelectWallet -> state.copy(selectedCurrency = action.walletData?.currency) + is WalletAction.MultiWallet.SetIsMultiwalletAllowed -> + state.copy(isMultiwalletAllowed = action.isMultiwalletAllowed) + + is WalletAction.MultiWallet.SelectWallet -> { + state.copy(selectedCurrency = action.currency) + } + is WalletAction.MultiWallet.SetSingleWalletCurrency -> { + state.copy(selectedCurrency = action.currency) + } is WalletAction.MultiWallet.TryToRemoveWallet -> state - is WalletAction.MultiWallet.RemoveWallet -> state.removeWallet(state.getWalletData(action.currency)) + is WalletAction.MultiWallet.RemoveWallet -> { + state.removeWalletData(state.getWalletData(action.currency)) + } is WalletAction.MultiWallet.RemoveWallets -> { var updatedState = state - action.currencies.forEach { updatedState = updatedState.removeWallet(state.getWalletData(it)) } + action.currencies.forEach { updatedState = updatedState.removeWalletData(state.getWalletData(it)) } updatedState } is WalletAction.MultiWallet.SetPrimaryBlockchain -> state.copy(primaryBlockchain = action.blockchain) @@ -205,10 +217,10 @@ fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletDat currencyData = BalanceWidgetData( status = BalanceStatus.Loading, currency = this.name, - currencySymbol = this.symbol + currencySymbol = this.symbol, ), walletAddresses = walletAddresses, mainButton = WalletMainButton.SendButton(false), - currency = currency + currency = currency, ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt index 79e84eec36..68e6080883 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt @@ -17,6 +17,7 @@ import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletMainButton import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN +import com.tangem.tap.features.wallet.redux.replaceSomeWalletsData import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData import com.tangem.tap.features.wallet.ui.TokenData @@ -106,18 +107,15 @@ class OnWalletLoadedReducer { mainButton = WalletMainButton.SendButton(isTokenSendButtonEnabled), ) } - val newWallets = tokens + newWalletData - val wallets = walletState.replaceSomeWallets((newWallets)) + val newWalletsData = tokens + newWalletData + val walletsData = walletState.walletsDataFromStores.replaceSomeWalletsData(newWalletsData) - return walletState - .updateWalletsData(wallets) + return walletState.updateWalletsData(walletsData) } private fun onSingleWalletLoaded(wallet: Wallet, walletState: WalletState): WalletState { if (wallet.blockchain != walletState.primaryBlockchain) return walletState - // val ratesRespository = store.state.globalState.tapWalletManager.ratesRepository - // val tokenCurrency = val fiatCurrencyName = store.state.globalState.appCurrency.code val token = wallet.getFirstToken() val tokenData = if (token != null) { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt index 79470dae62..08a7def4bd 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt @@ -36,6 +36,7 @@ private fun List.mapToProgressState(): List { BalanceStatus.Unreachable, BalanceStatus.EmptyCard, BalanceStatus.UnknownBlockchain, + BalanceStatus.MissedDerivation, -> ProgressState.Error BalanceStatus.Loading, null, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index 61ea00554a..254fe48e3b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -3,29 +3,38 @@ package com.tangem.tap.features.wallet.redux.reducers import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Wallet -import com.tangem.common.extensions.isZero import com.tangem.common.extensions.mapNotNullValues import com.tangem.domain.common.CardDTO import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.TwinCardNumber import com.tangem.tap.common.entities.FiatCurrency -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.toFiatRateString +import com.tangem.tap.common.extensions.toFiatString +import com.tangem.tap.common.extensions.toFiatValue +import com.tangem.tap.common.extensions.toFormattedCurrencyString +import com.tangem.tap.common.extensions.toFormattedFiatValue import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.getArtworkUrl import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.domain.getFirstToken -import com.tangem.tap.domain.model.TotalFiatBalance -import com.tangem.tap.domain.model.WalletDataModel -import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.TotalBalance import com.tangem.tap.features.wallet.models.WalletRent -import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.redux.AddressData +import com.tangem.tap.features.wallet.redux.Artwork +import com.tangem.tap.features.wallet.redux.ErrorType +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletAddresses +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletMainButton +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.redux.WalletStore +import com.tangem.tap.features.wallet.redux.replaceSomeWalletsData import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData -import com.tangem.tap.features.wallet.ui.TokenData import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.store import org.rekotlin.Action @@ -63,7 +72,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS is WalletAction.EmptyWallet -> { newState = newState.copy( state = ProgressState.Done, - wallets = listOf( + walletsStores = listOf( WalletStore( walletManager = null, blockchainNetwork = BlockchainNetwork( @@ -85,7 +94,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS is WalletAction.LoadData.Failure -> { when (action.error) { is TapError.NoInternetConnection -> { - val wallets = newState.wallets + val wallets = newState.walletsStores .map { store -> store.copy( walletsData = store.walletsData.map { @@ -95,20 +104,19 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS ), ) }, - - ) + ) } newState = newState.copy( state = ProgressState.Error, error = ErrorType.NoInternetConnection, - wallets = wallets, + walletsStores = wallets, ) } is TapError.UnknownBlockchain -> { newState = newState.copy( state = ProgressState.Done, - wallets = listOf( + walletsStores = listOf( WalletStore( walletManager = null, blockchainNetwork = BlockchainNetwork( @@ -153,7 +161,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS BalanceStatus.Loading } if (action.blockchain == null) { - val wallets = newState.wallets.map { walletStore -> + val wallets = newState.walletsStores.map { walletStore -> walletStore.copy( walletsData = walletStore.walletsData.map { walletData -> walletData.copy( @@ -170,7 +178,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS } newState = newState.copy( state = ProgressState.Loading, - wallets = wallets, + walletsStores = wallets, ) } else { val walletManager = newState.getWalletManager(action.blockchain) ?: return newState @@ -178,7 +186,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS walletManager.cardTokens.map { Currency.fromBlockchainNetwork(action.blockchain, it) } - val newWallets = newState.walletsData.filter { currencies.contains(it.currency) } + val newWalletsData = newState.walletsDataFromStores.filter { currencies.contains(it.currency) } .map { wallet -> wallet.copy( currencyData = wallet.currencyData.copy( @@ -189,8 +197,8 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS mainButton = WalletMainButton.SendButton(false), ) } - val wallets = newState.replaceSomeWallets(newWallets) - val walletStore = newState.getWalletStore(action.blockchain)?.updateWallets(wallets) + val walletsData = newState.walletsDataFromStores.replaceSomeWalletsData(newWalletsData) + val walletStore = newState.getWalletStore(action.blockchain)?.updateWallets(walletsData) newState = newState.updateWalletStore(walletStore) } } @@ -260,7 +268,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS newState = newState.updateWalletsData(updatedWallets) val progressState = - if (newState.walletsData.any { it.currencyData.status == BalanceStatus.Loading }) { + if (newState.walletsDataFromStores.any { it.currencyData.status == BalanceStatus.Loading }) { ProgressState.Loading } else { ProgressState.Done @@ -292,15 +300,13 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS } is WalletAction.TradeCryptoAction -> return newState is WalletAction.ChangeSelectedAddress -> { - val selectedWalletData = newState.getWalletData(newState.selectedCurrency) - - val walletAddresses = - newState.getWalletData(selectedWalletData?.currency)?.walletAddresses - ?: return newState + val walletAddresses = newState.getWalletData(newState.selectedCurrency)?.walletAddresses + ?: return newState val address = walletAddresses.list.firstOrNull { it.type == action.type } ?: return newState + newState = newState.updateWalletData( - selectedWalletData?.copy( + newState.selectedWalletData?.copy( walletAddresses = WalletAddresses( selectedAddress = address, list = walletAddresses.list, @@ -328,37 +334,41 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS val card = scanResponse.card newState = WalletState( cardId = card.cardId, - isMultiwalletAllowed = card.isMultiwalletAllowed, + isMultiwalletAllowed = isMultiCurrency, cardImage = Artwork( artworkId = artworkUrl, ), isTestnet = card.isTestCard, state = ProgressState.Loading, - wallets = newState.wallets, - showBackupWarning = card.isMultiwalletAllowed && + showBackupWarning = isMultiCurrency && card.settings.isBackupAllowed && card.backupStatus == CardDTO.BackupStatus.NoBackup, walletCardsCount = card.findCardsCount(), + totalBalance = if (isMultiCurrency) { + TotalBalance(ProgressState.Loading, BigDecimal.ZERO, store.state.globalState.appCurrency) + } else { + null + }, ) } - is WalletAction.WalletStoresChanged -> { + is WalletAction.WalletStoresChanged.UpdateWalletStores -> { newState = newState.copy( - wallets = action.walletStores.mapToReduxModel(newState.isMultiwalletAllowed), + walletsStores = action.reduxWalletStores, ) } is WalletAction.TotalFiatBalanceChanged -> { newState = newState.copy( - totalBalance = action.balance.mapToReduxModel(), + totalBalance = action.balance, ) } is WalletAction.LoadData.Success -> { val selectedCurrency = if (!newState.isMultiwalletAllowed) { - newState.wallets.firstOrNull() + newState.walletsStores.firstOrNull() ?.walletsData ?.firstOrNull() ?.currency } else { - newState.selectedCurrency + newState.selectedWalletData?.currency } newState = newState.copy( @@ -377,117 +387,6 @@ private fun CardDTO.findCardsCount(): Int? { ?.takeIf { this.isMultiwalletAllowed } } -@JvmName("walletStoreModelToReduxModel") -private fun List.mapToReduxModel( - isMultiWalletAllowed: Boolean, -): List { - return this.map { walletStoreModel -> - with(walletStoreModel) { - WalletStore( - walletManager = walletManager, - blockchainNetwork = blockchainNetwork, - walletsData = walletsData.mapToReduxModel(isMultiWalletAllowed, walletStoreModel.walletRent), - ) - } - } -} - -@JvmName("walletDataModelToReduxModel") -private fun List.mapToReduxModel( - isMultiWalletAllowed: Boolean, - walletRent: WalletStoreModel.WalletRent?, -): List { - return this.map { walletDataModel -> - with(walletDataModel) { - val amount = status.amount - val amountFormatted = amount.toFormattedCurrencyString( - decimals = currency.decimals, - currency = currency.currencySymbol, - ) - val appCurrency = store.state.globalState.appCurrency - val fiatAmount = fiatRate?.let { status.amount.toFiatValue(it) } - val fiatAmountFormatted = fiatAmount - ?.takeIf { !status.isErrorStatus } - ?.toFormattedFiatValue(appCurrency.symbol) - val fiatRateFormatted = fiatRate?.toFiatRateString(appCurrency.symbol) - - WalletData( - currency = currency, - walletAddresses = walletAddresses.getOrNull(0)?.let { selectedAddress -> - WalletAddresses( - selectedAddress = selectedAddress, - list = walletAddresses, - ) - }, - existentialDepositString = existentialDeposit?.toPlainString(), - fiatRate = fiatRate, - fiatRateString = fiatRateFormatted, - pendingTransactions = status.pendingTransactions, - mainButton = WalletMainButton.SendButton( - enabled = !status.amount.isZero() && status.pendingTransactions.isEmpty(), - ), - walletRent = walletRent?.let { - WalletRent( - minRentValue = "${it.rent.stripZeroPlainString()} ${currency.blockchain.currency}", - rentExemptValue = "${it.exemptionAmount.stripZeroPlainString()} ${currency.blockchain.currency}", - ) - }, - currencyData = BalanceWidgetData( - status = when (status) { - is WalletDataModel.Loading -> BalanceStatus.Loading - is WalletDataModel.NoAccount -> BalanceStatus.NoAccount - is WalletDataModel.Refreshing -> BalanceStatus.Refreshing - is WalletDataModel.SameCurrencyTransactionInProgress -> BalanceStatus.SameCurrencyTransactionInProgress - is WalletDataModel.TransactionInProgress -> BalanceStatus.TransactionInProgress - is WalletDataModel.Unreachable, - is WalletDataModel.MissedDerivation, - -> BalanceStatus.Unreachable - is WalletDataModel.VerifiedOnline -> BalanceStatus.VerifiedOnline - }, - currency = currency.currencyName, - currencySymbol = currency.currencySymbol, - blockchainAmount = status.amount, - amount = amount, - amountFormatted = amountFormatted, - fiatAmount = fiatAmount, - fiatAmountFormatted = fiatAmountFormatted, - token = when { - !isMultiWalletAllowed && currency is Currency.Token -> { - TokenData( - amount = amount, - amountFormatted = amountFormatted, - fiatAmount = fiatAmount, - fiatAmountFormatted = fiatAmountFormatted, - tokenSymbol = currency.currencySymbol, - fiatRate = fiatRate, - fiatRateString = fiatRateFormatted, - ) - } - else -> null - }, - amountToCreateAccount = (status as? WalletDataModel.NoAccount) - ?.amountToCreateAccount - ?.toString(), - errorMessage = status.errorMessage, - ), - ) - } - } -} - -private fun TotalFiatBalance.mapToReduxModel(): TotalBalance { - return TotalBalance( - state = when (this) { - is TotalFiatBalance.Loading -> ProgressState.Loading - is TotalFiatBalance.Refreshing -> ProgressState.Refreshing - is TotalFiatBalance.Error -> ProgressState.Error - is TotalFiatBalance.Loaded -> ProgressState.Done - }, - fiatAmount = amount, - fiatCurrency = store.state.globalState.appCurrency, - ) -} - fun createAddressList(wallet: Wallet?, walletAddresses: WalletAddresses? = null): WalletAddresses? { if (wallet == null) return null diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index 71bdbb7b8c..4afc4c08db 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -16,7 +16,8 @@ enum class BalanceStatus { Refreshing, NoAccount, EmptyCard, - UnknownBlockchain + UnknownBlockchain, + MissedDerivation, } data class BalanceWidgetData( diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index d737ac736b..a7f230dc35 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -1,16 +1,23 @@ package com.tangem.tap.features.wallet.ui import android.os.Bundle -import android.view.* +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup import android.widget.TextView import androidx.activity.OnBackPressedCallback import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding +import com.badoo.mvicore.modelWatcher +import com.tangem.common.doOnResult import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.tangem_sdk_new.extensions.dpToPx import com.tangem.tap.common.SnackbarHandler @@ -18,26 +25,38 @@ import com.tangem.tap.common.TestActions import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.events.DetailsScreen import com.tangem.tap.common.analytics.events.Token -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.appendIfNotNull +import com.tangem.tap.common.extensions.beginDelayedTransition +import com.tangem.tap.common.extensions.fitChipsByGroupWidth +import com.tangem.tap.common.extensions.getColor +import com.tangem.tap.common.extensions.getString +import com.tangem.tap.common.extensions.hide +import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.extensions.toQrCode import com.tangem.tap.common.recyclerView.SpaceItemDecoration import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.onboarding.getQRReceiveMessage import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.PendingTransaction -import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.redux.ErrorType +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter import com.tangem.tap.features.wallet.ui.adapters.WalletDetailWarningMessagesAdapter import com.tangem.tap.features.wallet.ui.images.load import com.tangem.tap.features.wallet.ui.test.TestWallet -import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.userWalletsListManagerSafe import com.tangem.tap.walletCurrenciesManager import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentWalletDetailsBinding +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.rekotlin.StoreSubscriber class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), @@ -48,6 +67,42 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), private val binding: FragmentWalletDetailsBinding by viewBinding(FragmentWalletDetailsBinding::bind) + private val walletDataWatcher = modelWatcher { + WalletData::pendingTransactions { + showPendingTransactionsIfPresent(it) + } + WalletData::currency { + handleCurrencyIcon(it) + } + WalletData::currencyData { + setupBalanceData(it) + } + (WalletData::currencyData or WalletData::currency) { walletData -> + setupCurrency(walletData.currencyData, walletData.currency) + setupSwipeRefresh(walletData.currencyData, walletData.currency) + } + } + + private val walletStateWatcher = modelWatcher { + (WalletState::selectedCurrency or WalletState::selectedWalletData) { state -> + val selectedWalletData = state.selectedWalletData + if (selectedWalletData != null) { + walletDataWatcher.invoke(selectedWalletData) + setupButtons(selectedWalletData, state.isExchangeServiceFeatureOn) + setupAddressCard(selectedWalletData) + handleWarnings(selectedWalletData) + } + } + (WalletState::selectedCurrency or WalletState::isExchangeServiceFeatureOn) { state -> + if (state.selectedWalletData != null) { + setupButtons(state.selectedWalletData!!, state.isExchangeServiceFeatureOn) + } + } + (WalletState::state or WalletState::error) { state -> + setupNoInternetHandling(state.state, state.error) + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) @@ -79,6 +134,12 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), store.unsubscribe(this) } + override fun onDestroy() { + walletDataWatcher.clear() + walletStateWatcher.clear() + super.onDestroy() + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar) @@ -129,50 +190,10 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), override fun newState(state: WalletState) { if (activity == null || view == null) return - if (state.selectedCurrency == null) return - val selectedWallet = state.getSelectedWalletData() ?: return + if (state.selectedWalletData == null) return + walletStateWatcher.invoke(state) - - showPendingTransactionsIfPresent(selectedWallet.pendingTransactions) - setupCurrency(selectedWallet.currencyData, selectedWallet.currency) - setupAddressCard(selectedWallet) - setupNoInternetHandling(state) - setupBalanceData(selectedWallet.currencyData) - setupButtons(selectedWallet, state.isExchangeServiceFeatureOn) - - handleCurrencyIcon(selectedWallet) - handleWarnings(selectedWallet) updateViewMeasurements() - - binding.srlWalletDetails.setOnRefreshListener { - if (selectedWallet.currencyData.status != BalanceStatus.Loading) { - Analytics.send(Token.Refreshed()) - val blockchainNetwork = BlockchainNetwork( - blockchain = selectedWallet.currency.blockchain, - derivationPath = selectedWallet.currency.derivationPath, - tokens = emptyList(), - ) - val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync - if (selectedUserWallet != null) scope.launch { - walletCurrenciesManager.update(selectedUserWallet, blockchainNetwork) - } else { - store.dispatch( - WalletAction.LoadWallet( - blockchain = BlockchainNetwork( - selectedWallet.currency.blockchain, - selectedWallet.currency.derivationPath, - emptyList(), - ), - ), - ) - store.dispatch(WalletAction.LoadFiatRate(coinsList = listOf(selectedWallet.currency))) - } - } - } - - if (selectedWallet.currencyData.status != BalanceStatus.Loading) { - binding.srlWalletDetails.isRefreshing = false - } } private fun updateViewMeasurements() { @@ -203,6 +224,37 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), } } + private fun setupSwipeRefresh(currencyData: BalanceWidgetData, currency: Currency) { + binding.srlWalletDetails.setOnRefreshListener { + if (currencyData.status != BalanceStatus.Loading && currencyData.status != BalanceStatus.Refreshing) { + Analytics.send(Token.Refreshed()) + lifecycleScope.launch(Dispatchers.Default) { + val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync + if (selectedUserWallet != null) { + walletCurrenciesManager.update(selectedUserWallet, currency) + .doOnResult { + withContext(Dispatchers.Main) { + binding.srlWalletDetails.isRefreshing = false + } + } + } else { + val blockchainNetwork = BlockchainNetwork( + blockchain = currency.blockchain, + derivationPath = currency.derivationPath, + tokens = emptyList(), + ) + + store.dispatch(WalletAction.LoadWallet(blockchainNetwork)) + store.dispatch(WalletAction.LoadFiatRate(coinsList = listOf(currency))) + } + } + } + } + + binding.srlWalletDetails.isRefreshing = currencyData.status == BalanceStatus.Loading || + currencyData.status == BalanceStatus.Refreshing + } + private fun setupButtons(selectedWallet: WalletData, isExchangeServiceFeatureOn: Boolean) = with(binding) { lWalletDetails.btnCopy.setOnClickListener { selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString -> @@ -231,9 +283,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), rvWarningMessages.show(warningDetails.isNotEmpty()) } - private fun handleCurrencyIcon(wallet: WalletData) = with(binding.lWalletDetails.lBalance) { + private fun handleCurrencyIcon(currency: Currency) = with(binding.lWalletDetails.lBalance) { ivCurrency.load( - currency = wallet.currency, + currency = currency, derivationStyle = store.state.globalState .scanResponse ?.card @@ -281,9 +333,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), } } - private fun setupNoInternetHandling(state: WalletState) { - if (state.state == ProgressState.Error) { - if (state.error == ErrorType.NoInternetConnection) { + private fun setupNoInternetHandling(progressState: ProgressState, errorType: ErrorType?) { + if (progressState == ProgressState.Error) { + if (errorType == ErrorType.NoInternetConnection) { binding.srlWalletDetails.isRefreshing = false (activity as? SnackbarHandler)?.showSnackbar( text = R.string.wallet_notification_no_internet, @@ -350,7 +402,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_remove -> { - store.state.walletState.getSelectedWalletData()?.let { walletData -> + store.state.walletState.selectedWalletData?.let { walletData -> store.dispatch(WalletAction.MultiWallet.TryToRemoveWallet(walletData.currency)) true } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index 962c7cd390..c0bde194bf 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -15,6 +15,7 @@ import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding import coil.load import coil.size.Scale +import com.tangem.core.ui.fragments.setStatusBarColor import com.tangem.domain.common.TapWorkarounds.isSaltPay import com.tangem.tap.MainActivity import com.tangem.tap.common.analytics.Analytics @@ -80,15 +81,18 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber state.select { it.walletState } } walletView.setFragment(this, binding) - viewModel.launch() } override fun onStop() { @@ -97,6 +101,11 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber(D BalanceStatus.Unreachable -> { root.getString(R.string.wallet_balance_blockchain_unreachable) } + BalanceStatus.MissedDerivation -> { + root.getString(R.string.wallet_balance_missing_derivation) + } else -> null } @@ -95,7 +98,7 @@ class WalletAdapter : ListAdapter(D if (wallet.walletAddresses != null) { cardWallet.setOnClickListener { Analytics.send(Portfolio.TokenTapped()) - store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet)) + store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet.currency)) } } else { cardWallet.setOnClickListener(null) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/test/TestWalletDetailsActions.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/test/TestWalletDetailsActions.kt index 4f384d33f7..857cdd2631 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/test/TestWalletDetailsActions.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/test/TestWalletDetailsActions.kt @@ -156,7 +156,7 @@ private class SolanaRentWarningActionEmitter { } private fun getBlockchainNetwork(): BlockchainNetwork { - val currency = store.state.walletState.getSelectedWalletData()!!.currency + val currency = store.state.walletState.selectedWalletData!!.currency return BlockchainNetwork(currency.blockchain, currency.derivationPath, listOf()) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 1ceda1b7b4..f7c8dc8a66 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -3,6 +3,8 @@ package com.tangem.tap.features.wallet.ui.wallet import android.widget.Button import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager +import com.badoo.mvicore.DiffStrategy +import com.badoo.mvicore.modelWatcher import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.tap.common.analytics.Analytics @@ -33,6 +35,45 @@ class MultiWalletView : WalletView() { private lateinit var walletsAdapter: WalletAdapter + private val watcher = modelWatcher { + val totalBalanceStrategy: DiffStrategy = { old, new -> + old.cardId != new.cardId || + old.totalBalance != new.totalBalance || + old.state != new.state || + old.walletsStores.size != new.walletsStores.size + } + + // !!! Workaround !!! + // Checking state properties instead of state params can reduce application performance, + // but here it is necessary because the WalletStore has an unsuitable equals method + WalletState::walletsDataFromStores { + walletsAdapter.submitList(it) + } + WalletState::loadingUserTokens { + binding?.pbLoadingUserTokens?.show(it) + } + WalletState::walletCardsCount { walletCardsCount -> + binding?.let { + setupWalletCardNumber(it, walletCardsCount) + } + } + WalletState::missingDerivations { missingDerivations -> + binding?.let { + handleRescanWarning(it, missingDerivations.isNotEmpty()) + } + } + WalletState::showBackupWarning { showBackupWarnings -> + binding?.let { + handleBackupWarning(it, showBackupWarnings) + } + } + watch({ it }, totalBalanceStrategy) { walletState -> + binding?.let { + handleTotalBalance(it, walletState.totalBalance, walletState.state, walletState.walletsDataFromStores.size) + } + } + } + override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) { setFragment(fragment, binding) onViewCreated() @@ -40,6 +81,7 @@ class MultiWalletView : WalletView() { } private fun showMultiWalletView(binding: FragmentWalletBinding) = with(binding) { + watcher.clear() tvTwinCardNumber.hide() rvPendingTransaction.hide() lCardBalance.root.hide() @@ -68,13 +110,7 @@ class MultiWalletView : WalletView() { val fragment = fragment ?: return val binding = binding ?: return - handleTotalBalance(binding, state.totalBalance, state.state, state.walletsData.size) - handleBackupWarning(binding, state.showBackupWarning) - handleRescanWarning(binding, state.missingDerivations.isNotEmpty()) - setupWalletCardNumber(binding, state.walletCardsCount) - walletsAdapter.submitList(state.walletsData) - - binding.pbLoadingUserTokens.show(state.loadingUserTokens) + watcher.invoke(state) binding.btnAddToken.setOnClickListener { val card = store.state.globalState.scanResponse!!.card @@ -91,7 +127,7 @@ class MultiWalletView : WalletView() { store.dispatch(TokensAction.AllowToAddTokens(true)) store.dispatch( TokensAction.SetAddedCurrencies( - wallets = state.walletsData, + wallets = state.walletsDataFromStores, derivationStyle = card.derivationStyle, ), ) @@ -139,21 +175,26 @@ class MultiWalletView : WalletView() { walletsCount: Int, ) = with(binding.lCardTotalBalance) { if (walletsCount == 0) { - root.isVisible = false + if (progressState != ProgressState.Loading) { + root.isVisible = false + } } else { if (totalBalance == null) { - veilBalance.animateVisibility(show = true) - root.isVisible = progressState == ProgressState.Loading + if (progressState != ProgressState.Loading) { + root.isVisible = false + } } else { root.isVisible = true - // Skip changes when on refreshing state if (totalBalance.state == ProgressState.Refreshing || progressState == ProgressState.Refreshing) { return@with } - veilBalance.animateVisibility(show = totalBalance.state == ProgressState.Loading) - tvBalance.animateVisibility(show = totalBalance.state != ProgressState.Loading) + if (totalBalance.state == ProgressState.Loading) { + veilBalance.veil() + } else { + veilBalance.unVeil() + } tvProcessing.animateVisibility(show = totalBalance.state == ProgressState.Error) tvBalance.text = totalBalance.fiatAmount.formatAmountAsSpannedString( @@ -218,6 +259,11 @@ class MultiWalletView : WalletView() { rowButtons.btnSend.text = fragment?.getText(R.string.wallet_button_create_wallet) rowButtons.onSendClick = { store.dispatch(WalletAction.CreateWallet) } } + + override fun onDestroyFragment() { + super.onDestroyFragment() + watcher.clear() + } } private val WalletDetailsButtonsRow.btnBuy: Button diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt index e765e738e8..44ff7329fc 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SaltPaySingleWalletView.kt @@ -52,4 +52,4 @@ class SaltPaySingleWalletView : WalletView() { ), ).setup() } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index abca20330d..e8fc90a242 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -153,14 +153,12 @@ class SingleWalletView : WalletView() { (binding.lAddress.root as? ViewGroup)?.beginDelayedTransition() chipGroupAddressType.show() chipGroupAddressType.fitChipsByGroupWidth() - val checkedId = - MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) + val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId) chipGroupAddressType.setOnCheckedChangeListener { group, checkedId -> if (checkedId == -1) return@setOnCheckedChangeListener - val type = - MultipleAddressUiHelper.idToType(checkedId, state.currency.blockchain) + val type = MultipleAddressUiHelper.idToType(checkedId, state.currency.blockchain) type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) } } } else { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt index a3b09aab12..d7d7cbb405 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt @@ -17,6 +17,8 @@ abstract class WalletView { binding = null } + open fun onDestroyFragment() {} + abstract fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) abstract fun onViewCreated() abstract fun onNewState(state: WalletState) diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/UserWalletModel.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/UserWalletModel.kt index 1417914782..b381b423c3 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/UserWalletModel.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/UserWalletModel.kt @@ -1,9 +1,10 @@ package com.tangem.tap.features.walletSelector.redux +import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.domain.model.TotalFiatBalance data class UserWalletModel( - val id: String, + val id: UserWalletId, val name: String, val artworkUrl: String, val type: Type, diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt index 957661126f..0985f4b68c 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt @@ -34,20 +34,16 @@ internal sealed interface WalletSelectorAction : Action { } data class SelectWallet( - val walletId: String, - ) : WalletSelectorAction - - data class UnlockWalletWithCard( - val walletId: String, + val userWalletId: UserWalletId, ) : WalletSelectorAction data class RenameWallet( - val walletId: String, + val userWalletId: UserWalletId, val newName: String, ) : WalletSelectorAction data class RemoveWallets( - val walletIdsToRemove: List, + val userWalletsIds: List, ) : WalletSelectorAction object AddWallet : WalletSelectorAction { diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt index dd4c4ff88b..b6b31419de 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt @@ -19,13 +19,16 @@ import com.tangem.tap.domain.model.UserWallet import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.domain.scanCard.ScanCardProcessor -import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager import com.tangem.tap.totalFiatBalanceCalculator import com.tangem.tap.userWalletsListManager import com.tangem.tap.walletStoresManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import org.rekotlin.Middleware import timber.log.Timber @@ -59,18 +62,17 @@ internal class WalletSelectorMiddleware { addWallet() } is WalletSelectorAction.SelectWallet -> { - selectWallet(action.walletId) - } - is WalletSelectorAction.UnlockWalletWithCard -> { - unlockWalletWithCard(action.walletId) + selectWallet(action.userWalletId) } is WalletSelectorAction.RemoveWallets -> { - removeWallets(action.walletIdsToRemove, state) + deleteWallets(action.userWalletsIds, state) } is WalletSelectorAction.RenameWallet -> { - renameWallet(action.walletId, action.newName) + renameWallet(action.userWalletId, action.newName) + } + is WalletSelectorAction.ChangeAppCurrency -> { + refreshUserWalletsAmounts() } - is WalletSelectorAction.ChangeAppCurrency, is WalletSelectorAction.AddWallet.Success, is WalletSelectorAction.AddWallet.Error, is WalletSelectorAction.SelectedWalletChanged, @@ -96,13 +98,17 @@ internal class WalletSelectorMiddleware { private fun updateBalances(walletStores: Map>, state: WalletSelectorState) { walletStores.forEach { (walletId, walletStores) -> - scope.launch { - val updatedWallet = state.wallets - .find { it.id == walletId.stringValue } - ?.updateWalletStoresAndCalculateFiatBalance(walletStores) + scope.launch(Dispatchers.Default) { + val foundWallet = state.wallets + .find { it.id == walletId } - if (updatedWallet != null) { - store.dispatchOnMain(WalletSelectorAction.BalanceLoaded(updatedWallet)) + if (foundWallet != null) { + val updatedWallet = foundWallet + .updateWalletStoresAndCalculateFiatBalance(walletStores) + + if (foundWallet != updatedWallet) { + store.dispatchOnMain(WalletSelectorAction.BalanceLoaded(updatedWallet)) + } } } } @@ -135,45 +141,38 @@ internal class WalletSelectorMiddleware { .doOnSuccess { Analytics.send(MyWallets.CardWasScanned) - userWalletsListManager.selectWallet(userWallet.walletId) store.dispatchOnMain(WalletSelectorAction.AddWallet.Success) - updateAccessCodeRequestPolicy(userWallet) store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) store.onUserWalletSelected(userWallet) } } } - private fun selectWallet(id: String) { + private fun selectWallet(userWalletId: UserWalletId) { scope.launch { - userWalletsListManager.selectWallet(UserWalletId(id)) - .doOnFailure { error -> - store.dispatchOnMain(WalletSelectorAction.HandleError(error)) - } - .doOnSuccess { selectedWallet -> - updateAccessCodeRequestPolicy(selectedWallet) - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) - store.onUserWalletSelected(selectedWallet) - } - } - } - - private fun unlockWalletWithCard(id: String) { - scope.launch { - userWalletsListManager.get(UserWalletId(id)) + userWalletsListManager.get(userWalletId) .flatMap { userWallet -> - updateUserWalletWithScannedCard(userWallet) - } - .flatMap { updatedUserWallet -> - unlockUserWallet(updatedUserWallet) + if (userWallet.isLocked) { + unlockUserWalletWithScannedCard(userWallet) + } else { + userWalletsListManager.selectWallet(userWalletId) + } } .doOnFailure { error -> store.dispatchOnMain(WalletSelectorAction.HandleError(error)) } + .doOnSuccess { + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync + if (selectedUserWallet != null) { + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + store.onUserWalletSelected(selectedUserWallet) + } + } } } - private suspend fun updateUserWalletWithScannedCard(userWallet: UserWallet): CompletionResult { + private suspend fun unlockUserWalletWithScannedCard(userWallet: UserWallet): CompletionResult { + tangemSdkManager.changeDisplayedCardIdNumbersCount(userWallet.scanResponse) return tangemSdkManager.scanCard(userWallet.cardId) .map { scannedCard -> userWallet.copy( @@ -182,23 +181,26 @@ internal class WalletSelectorMiddleware { ), ) } - } - - private suspend fun unlockUserWallet(userWallet: UserWallet): CompletionResult { - return userWalletsListManager.unlockWithCard(userWallet) - .doOnSuccess { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) - store.onUserWalletSelected(userWallet) + .flatMap { updatedUserWallet -> + userWalletsListManager.save(updatedUserWallet, canOverride = true) + } + .doOnFailure { + tangemSdkManager.changeDisplayedCardIdNumbersCount( + scanResponse = userWalletsListManager.selectedUserWalletSync?.scanResponse, + ) } } - private fun removeWallets(walletIdsToRemove: List, state: WalletSelectorState) { + private fun deleteWallets(userWalletsIds: List, state: WalletSelectorState) { Analytics.send(MyWallets.Button.DeleteWalletTapped) scope.launch { - when (walletIdsToRemove.size) { + when (userWalletsIds.size) { state.wallets.size -> clearUserWallets() - else -> removeUserWallets(walletIdsToRemove, state) + else -> deleteUserWallets( + userWalletsIds = userWalletsIds, + currentSelectedWalletId = state.selectedWalletId, + ) } .doOnFailure { error -> store.dispatchOnMain(WalletSelectorAction.HandleError(error)) @@ -206,11 +208,11 @@ internal class WalletSelectorMiddleware { } } - private fun renameWallet(walletId: String, newName: String) { + private fun renameWallet(userWalletId: UserWalletId, newName: String) { Analytics.send(MyWallets.Button.EditWalletTapped) scope.launch { - userWalletsListManager.get(walletId = UserWalletId(walletId)) + userWalletsListManager.get(userWalletId) .map { it.copy(name = newName) } .flatMap { userWalletsListManager.save(it, canOverride = true) } .doOnFailure { error -> @@ -219,6 +221,50 @@ internal class WalletSelectorMiddleware { } } + private fun refreshUserWalletsAmounts() { + scope.launch { + walletStoresManager.updateAmounts( + userWallets = userWalletsListManager.userWallets.first(), + ) + .doOnFailure { error -> + store.dispatchOnMain(WalletSelectorAction.HandleError(error)) + } + } + } + + private suspend fun clearUserWallets(): CompletionResult { + return userWalletsListManager.clear() + .flatMap { walletStoresManager.clear() } + .flatMap { tangemSdkManager.clearSavedUserCodes() } + .doOnSuccess { + // !!! Workaround !!! + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + delay(timeMillis = 280) + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) + } + } + + private suspend fun deleteUserWallets( + userWalletsIds: List, + currentSelectedWalletId: UserWalletId?, + ): CompletionResult { + return userWalletsListManager.delete(userWalletsIds) + .flatMap { walletStoresManager.delete(userWalletsIds) } + .flatMap { deleteAccessCodes(userWalletsIds) } + .doOnSuccess { + val selectedWallet = userWalletsListManager.selectedUserWalletSync + + when { + selectedWallet == null -> { + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome)) + } + currentSelectedWalletId != selectedWallet.walletId -> { + store.onUserWalletSelected(selectedWallet) + } + } + } + } + private suspend inline fun scanCardInternal( crossinline onCardScanned: suspend (ScanResponse) -> Unit, ) { @@ -236,37 +282,13 @@ internal class WalletSelectorMiddleware { ) } - private suspend fun clearUserWallets(): CompletionResult { - return userWalletsListManager.clear() - .flatMap { walletStoresManager.clear() } - .doOnSuccess { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) - } - } + private suspend fun deleteAccessCodes(userWalletsIds: List): CompletionResult { + val cardsIds = userWalletsListManager.userWallets.firstOrNull().orEmpty() + .asSequence() + .filter { it.walletId in userWalletsIds } + .flatMap { it.cardsInWallet } - private suspend fun removeUserWallets( - walletIdsToRemove: List, - state: WalletSelectorState, - ): CompletionResult { - val prevSelectedWalletId = state.selectedWalletId - return userWalletsListManager.delete(walletIdsToRemove.map { UserWalletId(it) }) - .flatMap { walletStoresManager.delete(walletIdsToRemove) } - .doOnSuccess { - val selectedWallet = userWalletsListManager.selectedUserWalletSync ?: return@doOnSuccess - val isSelectedWalletRemoved = prevSelectedWalletId != selectedWallet.walletId.stringValue - - if (isSelectedWalletRemoved) { - updateAccessCodeRequestPolicy(selectedWallet) - store.onUserWalletSelected(selectedWallet) - } - } - } - - private fun updateAccessCodeRequestPolicy(userWallet: UserWallet) { - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes && - userWallet.hasAccessCode, - ) + return tangemSdkManager.deleteSavedUserCodes(cardsIds.toSet()) } private suspend fun UserWalletModel.updateWalletStoresAndCalculateFiatBalance( diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt index 959c45c8d9..b64fe0a3ce 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt @@ -1,9 +1,7 @@ package com.tangem.tap.features.walletSelector.redux import com.tangem.domain.common.CardDTO -import com.tangem.tap.common.extensions.replaceByOrAdd import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.domain.model.TotalFiatBalance import com.tangem.tap.domain.model.UserWallet import org.rekotlin.Action @@ -21,7 +19,7 @@ internal object WalletSelectorReducer { wallets = action.userWallets.updateWalletsModels(state.wallets), ) is WalletSelectorAction.SelectedWalletChanged -> state.copy( - selectedWalletId = action.selectedWallet.walletId.stringValue, + selectedWalletId = action.selectedWallet.walletId, ) is WalletSelectorAction.IsLockedChanged -> state.copy( isLocked = action.isLocked, @@ -56,7 +54,6 @@ internal object WalletSelectorReducer { ) is WalletSelectorAction.WalletStoresChanged, is WalletSelectorAction.SelectWallet, - is WalletSelectorAction.UnlockWalletWithCard, is WalletSelectorAction.RemoveWallets, is WalletSelectorAction.RenameWallet, -> state @@ -66,7 +63,7 @@ internal object WalletSelectorReducer { private fun List.updateWalletsModels(prevWallets: List): List { return this.map { userWallet -> prevWallets - .find { it.id == userWallet.walletId.stringValue } + .find { it.id == userWallet.walletId } ?.let { it.copy( name = userWallet.name, @@ -77,7 +74,7 @@ internal object WalletSelectorReducer { } ?: with(userWallet) { UserWalletModel( - id = walletId.stringValue, + id = walletId, name = name, artworkUrl = artworkUrl, type = getType(), @@ -92,12 +89,17 @@ internal object WalletSelectorReducer { userWalletModel: UserWalletModel, ): List { return ArrayList(this).apply { - replaceByOrAdd(userWalletModel) { it.id == userWalletModel.id } + val index = indexOfFirst { it.id == userWalletModel.id } + if (index == -1) { + add(userWalletModel) + } else { + this[index] = userWalletModel + } } } private fun UserWallet.getType(prevType: UserWalletModel.Type? = null): UserWalletModel.Type { - return if (scanResponse.card.isMultiwalletAllowed) { + return if (isMultiCurrency) { UserWalletModel.Type.MultiCurrency( cardsInWallet = (scanResponse.card.backupStatus as? CardDTO.BackupStatus.Active) ?.cardCount?.inc() diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt index df6ec0867d..797cc04593 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt @@ -1,12 +1,13 @@ package com.tangem.tap.features.walletSelector.redux import com.tangem.common.core.TangemError +import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.common.entities.FiatCurrency import org.rekotlin.StateType data class WalletSelectorState( val wallets: List = emptyList(), - val selectedWalletId: String? = null, + val selectedWalletId: UserWalletId? = null, val isLocked: Boolean = false, val fiatCurrency: FiatCurrency = FiatCurrency.Default, val isCardSavingInProgress: Boolean = false, diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/Mapper.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/Mapper.kt index 6694b8b663..e5dc088a55 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/Mapper.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/Mapper.kt @@ -31,8 +31,8 @@ internal fun WalletSelectorScreenState.updateWithNewState( return this.copy( multiCurrencyWallets = multiCurrencyWallets, singleCurrencyWallets = singleCurrencyWallets, - selectedWalletId = newState.selectedWalletId, - editingWalletsIds = editingWalletsIds.filter { it in walletsIds }, + selectedUserWalletId = newState.selectedWalletId, + editingUserWalletsIds = editingUserWalletsIds.filter { it in walletsIds }, isLocked = newState.isLocked, showUnlockProgress = newState.isUnlockInProgress, showAddCardProgress = newState.isCardSavingInProgress, @@ -70,7 +70,7 @@ private fun List.toUiModels( imageUrl = artworkUrl, balance = balance, isLocked = isLocked, - tokenName = type.blockchainName ?: "—", + tokenName = type.blockchainName, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt index 717f82ed16..4db33980de 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt @@ -7,7 +7,13 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.SnackbarHost import androidx.compose.material.SnackbarHostState -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -39,18 +45,12 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment = emptyList(), val singleCurrencyWallets: List = emptyList(), - val selectedWalletId: String? = null, + val selectedUserWalletId: UserWalletId? = null, val isLocked: Boolean = false, - val editingWalletsIds: List = listOf(), + val editingUserWalletsIds: List = listOf(), val renameWalletDialog: RenameWalletDialog? = null, val showAddCardProgress: Boolean = false, val showUnlockProgress: Boolean = false, diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt index a7eaa3c4f4..1192b37c4a 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.walletSelector.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction import com.tangem.tap.features.walletSelector.redux.WalletSelectorState @@ -35,40 +36,38 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber { - store.dispatch(WalletSelectorAction.UnlockWalletWithCard(walletId)) + editingUserWalletsIds.isNotEmpty() && isWalletLocked(userWalletId, this) -> Unit + editingUserWalletsIds.isNotEmpty() && !editingUserWalletsIds.contains(userWalletId) -> { + editWallet(userWalletId) } - editingWalletsIds.isNotEmpty() && !editingWalletsIds.contains(walletId) -> { - editWallet(walletId) + editingUserWalletsIds.isNotEmpty() && editingUserWalletsIds.contains(userWalletId) -> { + cancelWalletEditing(userWalletId) } - editingWalletsIds.isNotEmpty() && editingWalletsIds.contains(walletId) -> { - cancelWalletEditing(walletId) - } - selectedWalletId != walletId -> { - store.dispatch(WalletSelectorAction.SelectWallet(walletId)) + selectedUserWalletId != userWalletId -> { + store.dispatch(WalletSelectorAction.SelectWallet(userWalletId)) } } } - fun walletLongClicked(walletId: String) = with(state.value) { - if (!isLocked && editingWalletsIds.isEmpty()) { - editWallet(walletId) + fun walletLongClicked(userWalletId: UserWalletId) = with(state.value) { + if (!isWalletLocked(userWalletId, this) && editingUserWalletsIds.isEmpty()) { + editWallet(userWalletId) } } fun cancelWalletsEditing() { stateInternal.update { prevState -> prevState.copy( - editingWalletsIds = emptyList(), + editingUserWalletsIds = emptyList(), ) } } fun renameWallet() = with(state.value) { - if (editingWalletsIds.isNotEmpty() && renameWalletDialog == null) { - val editedWalletId = editingWalletsIds.first() + if (editingUserWalletsIds.isNotEmpty() && renameWalletDialog == null) { + val editedWalletId = editingUserWalletsIds.first() val editedWallet = (multiCurrencyWallets + singleCurrencyWallets) .find { it.id == editedWalletId } @@ -80,7 +79,7 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber prevState.copy( renameWalletDialog = null, - editingWalletsIds = emptyList(), + editingUserWalletsIds = emptyList(), ) } }, @@ -103,8 +102,8 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber prevState.copy( - editingWalletsIds = prevState.editingWalletsIds + walletId, + editingUserWalletsIds = prevState.editingUserWalletsIds + userWalletId, ) } } - private fun cancelWalletEditing(walletId: String) { + private fun cancelWalletEditing(userWalletId: UserWalletId) { stateInternal.update { prevState -> prevState.copy( - editingWalletsIds = prevState.editingWalletsIds - walletId, + editingUserWalletsIds = prevState.editingUserWalletsIds - userWalletId, ) } } + private fun isWalletLocked(userWalletId: UserWalletId, state: WalletSelectorScreenState): Boolean = with(state) { + multiCurrencyWallets.find { it.id == userWalletId }?.isLocked + ?: singleCurrencyWallets.find { it.id == userWalletId }?.isLocked ?: isLocked + } + private fun subscribeToStoreChanges() { store.subscribe(this) { appState -> appState.skip { old, new -> old.walletSelectorState == new.walletSelectorState } diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/MockData.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/MockData.kt index 2fab91adc1..65349386dc 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/MockData.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/MockData.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.walletSelector.ui.components +import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.features.walletSelector.ui.WalletSelectorScreenState import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem @@ -7,7 +8,7 @@ import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem internal object MockData { private val multiCurrencyUserWallet = MultiCurrencyUserWalletItem( - id = "wallet_1", + id = UserWalletId("wallet_1"), balance = UserWalletItem.Balance( amount = "6781.05 $", isLoading = false, @@ -20,7 +21,7 @@ internal object MockData { ) private val singleCurrencyUserWallet = SingleCurrencyUserWalletItem( - id = "wallet_4", + id = UserWalletId("wallet_4"), balance = UserWalletItem.Balance( amount = "6781.05 $", isLoading = false, @@ -34,11 +35,11 @@ internal object MockData { val state = WalletSelectorScreenState( multiCurrencyWallets = listOf( multiCurrencyUserWallet, - multiCurrencyUserWallet.copy(id = "wallet_2"), - multiCurrencyUserWallet.copy(id = "wallet_3", tokensCount = 2, cardsInWallet = 1), + multiCurrencyUserWallet.copy(id = UserWalletId("wallet_2")), + multiCurrencyUserWallet.copy(id = UserWalletId("wallet_3"), tokensCount = 2, cardsInWallet = 1), ), singleCurrencyWallets = listOf(singleCurrencyUserWallet), - selectedWalletId = multiCurrencyUserWallet.id, + selectedUserWalletId = multiCurrencyUserWallet.id, isLocked = false, ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt index 136f786705..23ea8c4c9e 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt @@ -1,8 +1,8 @@ package com.tangem.tap.features.walletSelector.ui.components +import androidx.annotation.StringRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -11,8 +11,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Text @@ -23,61 +23,84 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButtonIconRight -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.details.ui.cardsettings.resolveReference +import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.features.walletSelector.ui.WalletSelectorScreenState -import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem import com.tangem.wallet.R +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun WalletSelectorScreenContent( - modifier: Modifier = Modifier, state: WalletSelectorScreenState, - onWalletClick: (walletId: String) -> Unit, - onWalletLongClick: (walletId: String) -> Unit, + onWalletClick: (UserWalletId) -> Unit, + onWalletLongClick: (UserWalletId) -> Unit, onUnlockClick: () -> Unit, onAddCardClick: () -> Unit, onClearSelectedClick: () -> Unit, onEditSelectedWalletClick: () -> Unit, onDeleteSelectedWalletsClick: () -> Unit, ) { - Column(modifier = modifier) { - Header( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - editingWalletsIds = state.editingWalletsIds, - onClearSelectedClick = onClearSelectedClick, - onEditSelectedWalletClick = onEditSelectedWalletClick, - onDeleteSelectedWalletsClick = onDeleteSelectedWalletsClick, - ) - Column( - modifier = Modifier - .verticalScroll( - state = rememberScrollState(), - ), - ) { - WalletsList( - multiCurrencyWallets = state.multiCurrencyWallets, - singleCurrencyWallets = state.singleCurrencyWallets, - selectedWalletId = state.selectedWalletId, - checkedWalletIds = state.editingWalletsIds, + LazyColumn { + stickyHeader { + Header( + editingWalletsIds = state.editingUserWalletsIds, + onClearSelectedClick = onClearSelectedClick, + onEditSelectedWalletClick = onEditSelectedWalletClick, + onDeleteSelectedWalletsClick = onDeleteSelectedWalletsClick, + ) + } + + item { + WalletsTitle(textResId = R.string.user_wallet_list_multi_header, wallets = state.multiCurrencyWallets) + } + + itemsIndexed( + items = state.multiCurrencyWallets, + key = { _, wallet -> wallet.id.stringValue }, + ) { _, wallet -> + + WalletItem( + wallet = wallet, + isSelected = remember(state.selectedUserWalletId) { wallet.id == state.selectedUserWalletId }, + isChecked = remember(state.editingUserWalletsIds) { wallet.id in state.editingUserWalletsIds }, onWalletClick = onWalletClick, onWalletLongClick = onWalletLongClick, ) - SpacerH24() + } + + item { + WalletsTitle(textResId = R.string.user_wallet_list_single_header, wallets = state.singleCurrencyWallets) + } + + itemsIndexed( + items = state.singleCurrencyWallets, + key = { _, wallet -> wallet.id.stringValue }, + ) { _, wallet -> + WalletItem( + wallet = wallet, + isSelected = remember(state.selectedUserWalletId) { wallet.id == state.selectedUserWalletId }, + isChecked = remember(state.editingUserWalletsIds) { wallet.id in state.editingUserWalletsIds }, + onWalletClick = onWalletClick, + onWalletLongClick = onWalletLongClick, + ) + } + + item { Footer( modifier = Modifier + .padding( + top = dimensionResource(id = R.dimen.spacing24), + bottom = dimensionResource(id = R.dimen.spacing16), + ) .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), isLocked = state.isLocked, @@ -86,25 +109,28 @@ internal fun WalletSelectorScreenContent( onUnlockClick = onUnlockClick, onAddCardClick = onAddCardClick, ) - SpacerH16() } } } @Composable private fun Header( - modifier: Modifier = Modifier, - editingWalletsIds: List, + editingWalletsIds: List, onClearSelectedClick: () -> Unit, onEditSelectedWalletClick: () -> Unit, onDeleteSelectedWalletsClick: () -> Unit, ) { val editingWalletsSize by rememberUpdatedState(newValue = editingWalletsIds.size) - val hasEditingWallets by remember { - derivedStateOf { editingWalletsSize > 0 } - } + val hasEditingWallets by remember { derivedStateOf { editingWalletsSize > 0 } } - Column(modifier = modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + color = TangemTheme.colors.background.plain, + shape = TangemTheme.shapes.bottomSheet, + ), + ) { Hand() Box( modifier = Modifier @@ -114,7 +140,9 @@ private fun Header( ) { if (hasEditingWallets) { EditWalletsBar( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), editingWalletsSize = editingWalletsSize, onClearSelectedClick = onClearSelectedClick, onEditSelectedWalletClick = onEditSelectedWalletClick, @@ -132,44 +160,15 @@ private fun Header( } } -@OptIn(ExperimentalFoundationApi::class) @Composable -private fun WalletsList( - modifier: Modifier = Modifier, - multiCurrencyWallets: List, - singleCurrencyWallets: List, - selectedWalletId: String?, - checkedWalletIds: List, - onWalletClick: (walletId: String) -> Unit, - onWalletLongClick: (walletId: String) -> Unit, -) { - Column(modifier = modifier) { - val walletsSection = @Composable { wallets: List -> - wallets.forEachIndexed { index, wallet -> - if (index == 0) { - Text( - modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), - text = wallet.headerText.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - } - WalletItem( - modifier = Modifier - .combinedClickable( - onClick = { onWalletClick(wallet.id) }, - onLongClick = { onWalletLongClick(wallet.id) }, - ) - .padding(all = TangemTheme.dimens.spacing16), - wallet = wallet, - isSelected = wallet.id == selectedWalletId, - isChecked = wallet.id in checkedWalletIds, - ) - } - } - - walletsSection(multiCurrencyWallets) - walletsSection(singleCurrencyWallets) +private fun WalletsTitle(@StringRes textResId: Int, wallets: List<*>) { + if (wallets.isNotEmpty()) { + Text( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), + text = stringResource(id = textResId), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) } } @@ -278,11 +277,6 @@ private fun WalletSelectorScreenContentSample( .background(color = TangemTheme.colors.background.primary), ) { WalletSelectorScreenContent( - modifier = Modifier - .background( - color = TangemTheme.colors.background.plain, - shape = TangemTheme.shapes.bottomSheet, - ), state = MockData.state.copy(isLocked = true), onWalletClick = { /* no-op */ }, onWalletLongClick = { /* no-op */ }, @@ -320,13 +314,8 @@ private fun WalletSelectorScreenContent_EditWallets_Sample( .background(TangemTheme.colors.background.primary), ) { WalletSelectorScreenContent( - modifier = Modifier - .background( - color = TangemTheme.colors.background.plain, - shape = TangemTheme.shapes.bottomSheet, - ), state = MockData.state - .copy(editingWalletsIds = listOf(MockData.state.multiCurrencyWallets[2].id)), + .copy(editingUserWalletsIds = listOf(MockData.state.multiCurrencyWallets[2].id)), onWalletClick = { /* no-op */ }, onWalletLongClick = { /* no-op */ }, onUnlockClick = { /* no-op */ }, diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt index 8ad4d7a2fc..3b6f021dba 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt @@ -1,7 +1,9 @@ package com.tangem.tap.features.walletSelector.ui.components +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon @@ -26,6 +28,7 @@ import com.tangem.core.ui.components.SpacerH2 import com.tangem.core.ui.components.SpacerW6 import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.common.compose.TangemTypography import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem @@ -33,15 +36,23 @@ import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem import com.tangem.wallet.R import com.valentinilk.shimmer.shimmer +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun WalletItem( - modifier: Modifier = Modifier, wallet: UserWalletItem, isSelected: Boolean, isChecked: Boolean, + onWalletClick: (UserWalletId) -> Unit, + onWalletLongClick: (UserWalletId) -> Unit, ) { Row( - modifier = modifier, + modifier = Modifier + .combinedClickable( + onClick = { onWalletClick(wallet.id) }, + onLongClick = { onWalletLongClick(wallet.id) }, + ) + .height(72.dp) + .padding(all = TangemTheme.dimens.spacing16), verticalAlignment = Alignment.CenterVertically, ) { WalletCardImage( diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/UserWalletItem.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/UserWalletItem.kt index c546cd131e..ddd7518ca6 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/UserWalletItem.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/UserWalletItem.kt @@ -1,21 +1,14 @@ package com.tangem.tap.features.walletSelector.ui.model -import com.tangem.tap.features.details.ui.cardsettings.TextReference -import com.tangem.wallet.R +import com.tangem.domain.common.util.UserWalletId internal sealed interface UserWalletItem { - val id: String + val id: UserWalletId val name: String val imageUrl: String val balance: Balance val isLocked: Boolean - val headerText: TextReference - get() = when (this) { - is MultiCurrencyUserWalletItem -> TextReference.Res(R.string.user_wallet_list_multi_header) - is SingleCurrencyUserWalletItem -> TextReference.Res(R.string.user_wallet_list_single_header) - } - data class Balance( val amount: String, val isLoading: Boolean, @@ -23,7 +16,7 @@ internal sealed interface UserWalletItem { } internal data class MultiCurrencyUserWalletItem( - override val id: String, + override val id: UserWalletId, override val name: String, override val imageUrl: String, override val balance: UserWalletItem.Balance, @@ -33,7 +26,7 @@ internal data class MultiCurrencyUserWalletItem( ) : UserWalletItem internal data class SingleCurrencyUserWalletItem( - override val id: String, + override val id: UserWalletId, override val name: String, override val imageUrl: String, override val balance: UserWalletItem.Balance, 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 fb5b8a626d..7b0f37e36e 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 @@ -59,11 +59,6 @@ internal class WelcomeMiddleware { } .doOnSuccess { selectedUserWallet -> if (selectedUserWallet != null) { - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes && - selectedUserWallet.hasAccessCode, - ) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success) store.onUserWalletSelected(selectedUserWallet) @@ -78,8 +73,7 @@ internal class WelcomeMiddleware { scanCardInternal { scanResponse -> val userWallet = UserWalletBuilder(scanResponse).build() - tangemSdkManager.setAccessCodeRequestPolicy(useBiometricsForAccessCode = false) - userWalletsListManager.unlockWithCard(userWallet) + userWalletsListManager.save(userWallet, canOverride = true) .doOnFailure { error -> store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error)) } @@ -100,6 +94,9 @@ internal class WelcomeMiddleware { private suspend inline fun scanCardInternal( crossinline onCardScanned: suspend (ScanResponse) -> Unit, ) { + tangemSdkManager.setAccessCodeRequestPolicy( + useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, + ) ScanCardProcessor.scan( onSuccess = { scanResponse -> scope.launch { onCardScanned(scanResponse) } diff --git a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt index 2b4d40e0bb..7be322fc1e 100644 --- a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt @@ -108,7 +108,7 @@ class DerivationManagerImpl( val updatedScanResponse = scanResponse.copy( derivedKeys = updatedDerivedKeys, ) - appStateHolder.mainStore?.dispatchOnMain(GlobalAction.SaveScanNoteResponse(updatedScanResponse)) + appStateHolder.mainStore?.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse)) delay(DELAY_SDK_DIALOG_CLOSE) onSuccess(updatedScanResponse) } diff --git a/app/src/main/res/layout/fragment_wallet.xml b/app/src/main/res/layout/fragment_wallet.xml index ab388fa0cd..b3d1b54730 100644 --- a/app/src/main/res/layout/fragment_wallet.xml +++ b/app/src/main/res/layout/fragment_wallet.xml @@ -5,7 +5,7 @@ android:id="@+id/coordinator_wallet" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:orientation="vertical"> diff --git a/app/src/main/res/layout/fragment_wallet_details.xml b/app/src/main/res/layout/fragment_wallet_details.xml index 1467361b47..f1cdf9a73a 100644 --- a/app/src/main/res/layout/fragment_wallet_details.xml +++ b/app/src/main/res/layout/fragment_wallet_details.xml @@ -5,7 +5,7 @@ android:id="@+id/coordinator_wallet" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:orientation="vertical"> @@ -105,27 +105,27 @@ android:layout_marginStart="16dp" android:layout_marginEnd="16dp" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/l_wallet_details" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/l_wallet_details" /> + android:id="@+id/barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="l_wallet_details" /> + android:id="@+id/row_buttons" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="28dp" + android:layout_marginBottom="32dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + app:layout_constraintVertical_bias="1" + app:layout_constraintTop_toBottomOf="@id/rv_warning_messages" + app:layout_constraintBottom_toBottomOf="parent" /> diff --git a/app/src/main/res/layout/item_currency_wallet_content.xml b/app/src/main/res/layout/item_currency_wallet_content.xml index 18249f4c63..aa767d069c 100644 --- a/app/src/main/res/layout/item_currency_wallet_content.xml +++ b/app/src/main/res/layout/item_currency_wallet_content.xml @@ -14,77 +14,77 @@ app:layout_constraintGuide_percent="0.5" /> + android:id="@+id/tv_currency" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="@color/text_primary_1" + android:textSize="16sp" + android:textStyle="bold" + app:layout_constraintBottom_toTopOf="@id/guideline_horizontal" + app:layout_constraintEnd_toStartOf="@+id/tv_amount_fiat" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:lineHeight="24dp" + tools:text="Binance Smart Chain Optimal" /> + android:id="@+id/tv_amount_fiat" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:maxLines="1" + android:textAlignment="viewEnd" + android:textColor="@color/text_primary_1" + android:textSize="16sp" + android:textStyle="bold" + app:layout_constraintBottom_toTopOf="@id/guideline_horizontal" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:lineHeight="24dp" + tools:text="1230.43 $" /> + android:id="@+id/tv_amount" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:maxLines="1" + android:textAlignment="viewEnd" + android:textColor="@color/text_tertiary" + android:textSize="12sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/guideline_horizontal" + app:lineHeight="20dp" + tools:text="2.002134 BTC" /> + android:id="@+id/tv_status" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textColor="@color/text_tertiary" + android:textSize="12sp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/guideline_horizontal" + app:lineHeight="20dp" + tools:text="Unreachable..." /> + android:id="@+id/tv_exchange_rate" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:maxLines="1" + android:textColor="@color/text_secondary" + android:textSize="12sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/guideline_horizontal" + app:lineHeight="20dp" + tools:text="46 908 $" /> diff --git a/app/src/main/res/layout/layout_card_total_balance.xml b/app/src/main/res/layout/layout_card_total_balance.xml index 3f47e6b612..ed26db5407 100644 --- a/app/src/main/res/layout/layout_card_total_balance.xml +++ b/app/src/main/res/layout/layout_card_total_balance.xml @@ -12,8 +12,7 @@ android:layout_marginStart="16dp" android:layout_marginTop="18dp" android:layout_marginEnd="16dp" - android:layout_marginBottom="18dp" - android:animateLayoutChanges="true"> + android:layout_marginBottom="18dp"> - + app:layout_constraintTop_toBottomOf="@id/tv_title" + app:veilLayout_baseColor="@color/lightGray0" + app:veilLayout_highlightColor="@color/lightGray1" + app:veilLayout_layout="@layout/card_total_balance_shimmer" + app:veilLayout_radius="4dp" + app:veilLayout_shimmerEnable="true" + app:veilLayout_veiled="true"> - - - - + tools:text="22 325.40 $" + tools:visibility="visible" /> + diff --git a/buildSrc/src/main/java/Dependency.kt b/buildSrc/src/main/java/Dependency.kt index c1375a7b76..54ee97cd6d 100644 --- a/buildSrc/src/main/java/Dependency.kt +++ b/buildSrc/src/main/java/Dependency.kt @@ -66,6 +66,7 @@ object Library { const val zendeskMessaging = "com.zendesk:messaging:" + Versions.zendeskMessaging const val zxingQrBarcodeScanner = "me.dm7.barcodescanner:zxing:" + Versions.zxingQrBarcodeScanner const val zxingQrCore = "com.google.zxing:core:" + Versions.zxingQrCode + const val mviCoreWatcher = "com.github.badoo.mvicore:mvicore-diff:" + Versions.mviCore } object Tangem { diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index 3a15fb0a04..f63622d48b 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -55,13 +55,14 @@ object Versions { const val zendeskMessaging = "5.2.4" const val zxingQrBarcodeScanner = "1.9.8" const val zxingQrCode = "3.3.3" + const val mviCore = "1.3.1" // endregion Other libraries // region Tangem const val tangemBlockchainSdk = "develop-142" // const val tangemBlockchainSdk = "0.0.1" - const val tangemCardSgk = "develop-173" + const val tangemCardSgk = "develop-178" // endregion Tangem // region Testing diff --git a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt index 2a7d4c0864..35dab6bca0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt @@ -18,7 +18,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.tangem.core.ui.R -import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment() { @@ -52,13 +51,6 @@ abstract class ComposeBottomSheetFragment : BottomSheetDialogFragme return ComposeView(context).apply { setContent { TangemTheme { - val backgroundColor = TangemTheme.colors.background.plain - SystemBarsEffect { - setSystemBarsColor( - color = backgroundColor, - ) - } - ScreenContent( modifier = Modifier .fillMaxWidth() @@ -66,7 +58,7 @@ abstract class ComposeBottomSheetFragment : BottomSheetDialogFragme if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it } .background( - color = backgroundColor, + color = TangemTheme.colors.background.plain, shape = TangemTheme.shapes.bottomSheet, ), state = provideState().value, diff --git a/core/ui/src/main/java/com/tangem/core/ui/fragments/FragmentExt.kt b/core/ui/src/main/java/com/tangem/core/ui/fragments/FragmentExt.kt new file mode 100644 index 0000000000..0de62569bb --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/fragments/FragmentExt.kt @@ -0,0 +1,14 @@ +package com.tangem.core.ui.fragments + +import android.view.WindowManager +import androidx.annotation.ColorRes +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment + +fun Fragment.setStatusBarColor(@ColorRes colorResId: Int) { + with(requireActivity().window) { + clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) + addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + statusBarColor = ContextCompat.getColor(requireContext(), colorResId) + } +} \ No newline at end of file