Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-26 15:06:51 +04:00
parent d1798a2c49
commit 6dee2eeed7
112 changed files with 2146 additions and 1562 deletions

View file

@ -198,6 +198,7 @@ dependencies {
implementation(Library.armadillo)
implementation(Library.googlePlayServicesWallet)
implementation(Library.composeShimmer)
implementation(Library.mviCoreWatcher)
/** Testing libraries */
testImplementation(Test.junit)

View file

@ -101,6 +101,17 @@
<data android:scheme="wc" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="wc" />
<data android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

View file

@ -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="
}
}

View file

@ -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<Wal
private fun statesIsReadyToCreateEvent(scanResponse: ScanResponse, state: WalletState): Boolean {
if (scanResponse.card.isMultiwalletAllowed && state.missingDerivations.isNotEmpty()) return false
if (state.walletsData.isEmpty()) return false
if (state.walletsDataFromStores.isEmpty()) return false
val totalBalanceState = state.totalBalance?.state ?: return false
if (totalBalanceState == ProgressState.Loading || totalBalanceState == ProgressState.Refreshing) return false
val balancesCount = state.walletsData.map {
val balancesCount = state.walletsDataFromStores.map {
if (it.currencyData.amount == null) 0 else 1
}.reduce { acc, i -> acc + i }
if (balancesCount != state.wallets.size) return false
if (balancesCount != state.walletsStores.size) return false
return true
}

View file

@ -62,4 +62,9 @@ fun String.toQrCode(): Bitmap {
return bmp
}
fun String.urlEncode(): String = Uri.encode(this)
fun String.urlEncode(): String = Uri.encode(this)
fun String.removePrefixOrNull(prefix: String): String? = when {
startsWith(prefix) -> substring(prefix.length)
else -> null
}

View file

@ -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<AppState> = { _, _ ->
{ 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,
)
}
}

View file

@ -93,6 +93,7 @@ data class AppState(
SaveWalletMiddleware().middleware,
WalletSelectorMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware,
)
}
}

View file

@ -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()

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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<String>): CompletionResult<Unit> {
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<String>): CompletionResult<Unit> {
return userCodeRepository.delete(cardsIds.toSet())
}
suspend fun clearSavedUserCodes(): CompletionResult<Unit> {
return createUserCodeRepository().clear()
return userCodeRepository.clear()
}
suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse> {
@ -166,9 +178,10 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
suspend fun scanCard(
cardId: String? = null,
allowRequestAccessCodeFromRepository: Boolean = false,
): CompletionResult<CardDTO> {
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,

View file

@ -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))

View file

@ -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()

View file

@ -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<String>,
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
}

View file

@ -35,12 +35,6 @@ data class WalletDataModel(
open val pendingTransactions: List<PendingTransaction> = 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<PendingTransaction>,
override val errorMessage: String?,
) : Status() {
override val isErrorStatus: Boolean = errorMessage != null
}
}

View file

@ -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<WalletDataModel>,
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<WalletDataModel>,
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
}

View file

@ -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<String> = 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<String>?) = 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"
}
}

View file

@ -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),
)
}
}

View file

@ -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)
}
},
),
)
}
}

View file

@ -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<WalletDataModel>.mapToStatus(): Sequence<TotalFiatBalanceStatus> {
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,
}

View file

@ -10,6 +10,9 @@ import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.operations.wallet.PurgeWalletCommand
class CreateFirstTwinWalletTask(private val firstCardId: String) : CardSessionRunnable<CreateWalletResponse> {
override val allowsRequestAccessCodeFromRepository: Boolean = false
override fun run(
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit,

View file

@ -20,6 +20,8 @@ class CreateSecondTwinWalletTask(
private val creatingWalletMessage: Message,
) : CardSessionRunnable<CreateWalletResponse> {
override val allowsRequestAccessCodeFromRepository: Boolean = false
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
val card = session.environment.card
val publicKey = card?.wallets?.firstOrNull()?.publicKey

View file

@ -13,6 +13,8 @@ class FinalizeTwinTask(
private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair,
) : CardSessionRunnable<ScanResponse> {
override val allowsRequestAccessCodeFromRepository: Boolean = false
override fun run(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,

View file

@ -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 -> {}
}

View file

@ -14,24 +14,23 @@ interface UserWalletsListManager {
val hasSavedUserWallets: Boolean
suspend fun unlockWithBiometry(): CompletionResult<UserWallet?>
suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult<Unit>
fun lock()
suspend fun selectWallet(walletId: UserWalletId): CompletionResult<UserWallet>
suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult<UserWallet>
/**
* 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<Unit>
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit>
suspend fun clear(): CompletionResult<Unit>
suspend fun get(walletId: UserWalletId): CompletionResult<UserWallet>
suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet>
companion object
}

View file

@ -28,21 +28,19 @@ internal class BiometricUserWalletsListManager(
override val userWallets: Flow<List<UserWallet>>
get() = state
.mapLatest { it.wallets }
.mapLatest { it.userWallets }
.distinctUntilChanged()
override val selectedUserWallet: Flow<UserWallet>
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<Boolean>
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<UserWallet?> {
return unlockWithBiometryInternal()
.map { selectedUserWalletSync }
}
override suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult<Unit> {
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<UserWallet> = catching {
if (state.value.selectedWalletId == walletId) {
return@catching findSelectedWallet()!!
override suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult<UserWallet> = 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<Unit> = 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<Unit> {
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<UserWalletId>): CompletionResult<Unit> {
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<UserWalletId>): CompletionResult<Unit> {
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<Unit> {
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<UserWallet> {
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> {
return catching {
state.value.wallets.first { it.walletId == walletId }
state.value.userWallets.first { it.walletId == userWalletId }
}
}
private suspend inline fun <reified T> withUnlock(
block: () -> CompletionResult<T>,
): CompletionResult<T> {
return (if (state.value.isLocked) unlockWithBiometryInternal() else CompletionResult.Success(Unit))
.flatMap { block() }
private suspend fun saveInternal(userWallet: UserWallet): CompletionResult<Unit> {
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<Unit> {
@ -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<UserWallet>,
): 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<UserWalletId>,
) {
val remainingWallets = state.value.wallets.filter {
private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List<UserWalletId>) {
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<UserWallet> = state.value.userWallets): UserWallet? {
return userWallets.find {
it.walletId == state.value.selectedUserWalletId
}
}
private data class State(
val encryptionKeys: List<UserWalletEncryptionKey> = emptyList(),
val wallets: List<UserWallet> = emptyList(),
val selectedWalletId: UserWalletId? = null,
val userWallets: List<UserWallet> = emptyList(),
val selectedUserWalletId: UserWalletId? = null,
val isLocked: Boolean = true,
)
}

View file

@ -26,15 +26,11 @@ class DummyUserWalletsListManager : UserWalletsListManager {
return CompletionResult.Success(null)
}
override suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override fun lock() {
/* no-op */
}
override suspend fun selectWallet(walletId: UserWalletId): CompletionResult<UserWallet> {
override suspend fun selectWallet(userWalletId: UserWalletId): CompletionResult<UserWallet> {
return catching {
error("Not implemented")
}
@ -44,7 +40,7 @@ class DummyUserWalletsListManager : UserWalletsListManager {
return CompletionResult.Success(Unit)
}
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit> {
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
@ -52,7 +48,7 @@ class DummyUserWalletsListManager : UserWalletsListManager {
return CompletionResult.Success(Unit)
}
override suspend fun get(walletId: UserWalletId): CompletionResult<UserWallet> {
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> {
return catching {
error("Not implemented")
}

View file

@ -17,4 +17,5 @@ internal data class UserWalletPublicInformation(
val artworkUrl: String,
val cardsInWallet: Set<String>,
val scanResponse: ScanResponse,
val isMultiCurrency: Boolean,
)

View file

@ -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<List<UserWalletEncryptionKey>>
/**
* 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<UserWalletEncryptionKey>): CompletionResult<Unit>
/**
* 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<UserWalletId>): CompletionResult<Unit>
/**
* Clear all encryption keys for user wallets. Biometric authentication not required
* @return [CompletionResult] of operation
* */
suspend fun clear(): CompletionResult<Unit>
}

View file

@ -12,4 +12,6 @@ internal interface UserWalletsPublicInformationRepository {
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
suspend fun clear(): CompletionResult<Unit>
fun isNotEmpty(): Boolean
}

View file

@ -12,5 +12,6 @@ internal interface UserWalletsSensitiveInformationRepository {
encryptionKeys: List<UserWalletEncryptionKey>,
): CompletionResult<Map<UserWalletId, UserWalletSensitiveInformation>>
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit>
suspend fun clear(): CompletionResult<Unit>
}

View file

@ -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<List<UserWalletEncryptionKey>> = moshi.adapter(
Types.newParameterizedType(List::class.java, UserWalletEncryptionKey::class.java),
private val encryptionKeyAdapter: JsonAdapter<UserWalletEncryptionKey> = moshi.adapter(
UserWalletEncryptionKey::class.java,
)
private val userWalletsIdsListAdapter: JsonAdapter<List<UserWalletId>> = moshi.adapter(
Types.newParameterizedType(List::class.java, UserWalletId::class.java),
)
override suspend fun getAll(): CompletionResult<List<UserWalletEncryptionKey>> {
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<UserWalletEncryptionKey>): CompletionResult<Unit> {
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<UserWalletId>): CompletionResult<Unit> {
return withContext(Dispatchers.IO) {
userWalletsIds.map { userWalletId ->
deleteEncryptionKey(userWalletId)
}
.fold()
.map { deleteUserWalletsIds(userWalletsIds) }
}
}
override suspend fun clear(): CompletionResult<Unit> {
return biometricStorage.delete(key = StorageKey.WalletEncryptionKeys.name)
return withContext(Dispatchers.IO) {
getUserWalletsIds()
.map { userWalletId ->
deleteEncryptionKey(userWalletId)
}
.fold()
.map {
clearUserWalletsIds()
}
}
}
private fun List<UserWalletEncryptionKey>.encode(): ByteArray {
return this.let(walletsKeysAdapter::toJson)
.encodeToByteArray(throwOnInvalidSequence = true)
private suspend fun getAllInternal(): CompletionResult<List<UserWalletEncryptionKey>> {
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<UserWalletEncryptionKey> {
return this?.decodeToString(throwOnInvalidSequence = true)
?.let(walletsKeysAdapter::fromJson)
.orEmpty()
private suspend fun getEncryptionKey(userWalletId: UserWalletId): CompletionResult<UserWalletEncryptionKey?> {
return biometricStorage.get(StorageKey.WalletEncryptionKey(userWalletId).name)
.map { it.decodeToKey() }
}
private enum class StorageKey {
WalletEncryptionKeys
private suspend fun storeEncryptionKey(encryptionKey: UserWalletEncryptionKey): CompletionResult<Unit> {
return biometricStorage.store(
key = StorageKey.WalletEncryptionKey(encryptionKey.walletId).name,
data = encryptionKey.encode(),
)
.map { storeUserWalletId(encryptionKey.walletId) }
}
private suspend fun deleteEncryptionKey(userWalletId: UserWalletId): CompletionResult<Unit> {
return biometricStorage.delete(StorageKey.WalletEncryptionKey(userWalletId).name)
}
private suspend fun getUserWalletsIds(): List<UserWalletId> {
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<UserWalletId>) {
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<UserWalletId>.encode(): ByteArray {
return withContext(Dispatchers.Default) {
this@encode
.let(userWalletsIdsListAdapter::toJson)
.encodeToByteArray(throwOnInvalidSequence = true)
}
}
private suspend fun ByteArray?.decodeToUserWalletsIds(): List<UserWalletId> {
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"
}
}
}

View file

@ -25,19 +25,21 @@ internal class DefaultUserWalletsPublicInformationRepository(
)
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
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<List<UserWalletPublicInformation>> = catching {
@ -50,20 +52,28 @@ internal class DefaultUserWalletsPublicInformationRepository(
}
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit> {
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<Unit> = 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")

View file

@ -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<UserWalletSensitiveInformation> = moshi.adapter(
UserWalletSensitiveInformation::class.java,
)
private val encryptedSensitiveInformationMapAdapter: JsonAdapter<Map<String, ByteArray>> = 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<Unit> {
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<UserWalletEncryptionKey>,
): CompletionResult<Map<UserWalletId, UserWalletSensitiveInformation>> {
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<UserWalletId, UserWalletSensitiveInformation>()
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<UserWalletId>): CompletionResult<Unit> = catching {
walletIds
.forEach { walletId ->
secureStorage.delete(StorageKey.SensitiveInformation(walletId).name)
}
override suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit> {
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<Unit> {
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<String, ByteArray> {
return withContext(Dispatchers.IO) {
secureStorage.get(StorageKey.UserWalletsSensitiveInformation.name)
.decodeToEncryptedSensitiveInformation()
}
}
private suspend fun saveInternal(sensitiveInformation: Map<String, ByteArray>) {
return withContext(Dispatchers.IO) {
secureStorage.store(
account = StorageKey.UserWalletsSensitiveInformation.name,
data = sensitiveInformation.encode(),
)
}
}
private suspend fun deleteInternal(userWalletsIds: List<UserWalletId>) {
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<String, ByteArray> {
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<String, ByteArray>.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}"
}
}

View file

@ -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,
)
}

View file

@ -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<Unit>
suspend fun addCurrencies(

View file

@ -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<Unit> {
val walletStore = walletStoresRepository.get(userWallet.walletId).first()
currency: Currency,
): CompletionResult<Unit> = 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<Currency>,
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
var newBlockchainNetworks = listOf<BlockchainNetwork>()
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<Currency>,
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
val card = userWallet.scanResponse.card
val remainingCurrencies = getSavedCurrencies(userWallet.walletId)
.filter { it !in currenciesToRemove }
val remainingBlockchains = remainingCurrencies
.filterIsInstance<Currency.Blockchain>()
.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<Currency>,
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
var remainingBlockchainsNetworks = emptyList<BlockchainNetwork>()
catching {
val card = userWallet.scanResponse.card
val savedCurrencies = withContext(Dispatchers.IO) {
userTokensRepository.getUserTokens(card)
}
val remainingCurrencies = arrayListOf<Currency>()
savedCurrencies.forEach { savedCurrency ->
if (savedCurrency !in currenciesToRemove) {
remainingCurrencies.add(savedCurrency)
private suspend fun getSavedCurrencies(userWalletId: UserWalletId): List<Currency> {
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<Currency>.toBlockchainNetworks(card: CardDTO): List<BlockchainNetwork> {
val blockchainNetworks = arrayListOf<BlockchainNetwork>()
val findDerivationPath: (currency: Currency) -> String? = { currency ->
currency.derivationPath
?: currency.blockchain.derivationPath(card.derivationStyle)
?.rawPath
private suspend fun saveUserCurrencies(card: CardDTO, currencies: List<Currency>) {
withContext(Dispatchers.IO) {
userTokensRepository.saveUserTokens(
card = card,
tokens = currencies,
)
}
}
for (currency in this.sortedByDescending { it.isBlockchain() }) {
private fun List<Currency>.addMissingBlockchains(card: CardDTO): List<Currency> {
val newCurrencies = arrayListOf<Currency>()
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<BlockchainNetwork>.updateWalletStores(
private suspend fun updateWalletStores(
userWallet: UserWallet,
blockchainNetworks: List<BlockchainNetwork>,
): CompletionResult<Unit> {
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
}
}

View file

@ -10,7 +10,7 @@ interface WalletStoresManager {
fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>>
fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>>
suspend fun delete(userWalletsIds: List<String>): CompletionResult<Unit>
suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit>
suspend fun clear(): CompletionResult<Unit>
suspend fun fetch(
@ -23,5 +23,9 @@ interface WalletStoresManager {
refresh: Boolean = false,
): CompletionResult<Unit>
suspend fun updateAmounts(
userWallets: List<UserWallet>,
): CompletionResult<Unit>
companion object
}

View file

@ -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<List<WalletStoreModel>> {
return walletStoresRepository.get(userWalletId)
.distinctUntilChanged()
}
override suspend fun delete(userWalletsIds: List<String>): CompletionResult<Unit> {
val walletIds = userWalletsIds.map { UserWalletId(it) }
return walletStoresRepository.delete(walletIds)
.flatMap { walletManagersRepository.delete(walletIds) }
override suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit> {
return walletStoresRepository.delete(userWalletsIds)
.flatMap { walletManagersRepository.delete(userWalletsIds) }
}
override suspend fun clear(): CompletionResult<Unit> {
@ -55,7 +56,7 @@ internal class DefaultWalletStoresManager(
override suspend fun fetch(
userWallets: List<UserWallet>,
refresh: Boolean,
): CompletionResult<Unit> {
): CompletionResult<Unit> = 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<UserWallet>()) { acc, data ->
.fold(arrayListOf<UserWallet>()) { 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<UserWallet> {
return if (userWallet.scanResponse.card.isMultiwalletAllowed) {
fetchMultiWallets(userWallet, refresh)
override suspend fun updateAmounts(userWallets: List<UserWallet>): CompletionResult<Unit> {
val fiatCurrency = appCurrencyProvider.invoke()
return walletAmountsRepository.updateAmountsForUserWallets(userWallets, fiatCurrency)
.doOnSuccess {
state.update { prevState ->
prevState.copy(
fiatCurrency = fiatCurrency,
)
}
}
}
private suspend fun fetchWalletsIfNeeded(userWallet: UserWallet): CompletionResult<UserWallet> {
return if (userWallet.isMultiCurrency) {
fetchMultiWallets(userWallet)
} else {
fetchSingleWallet(userWallet, refresh)
fetchSingleWallet(userWallet)
}
.map { userWallet }
}
private suspend fun fetchMultiWallets(
userWallet: UserWallet,
refresh: Boolean,
): CompletionResult<Unit> {
private suspend fun fetchMultiWallets(userWallet: UserWallet): CompletionResult<Unit> {
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<Unit> {
return walletManagersRepository.findOrMake(
private suspend fun fetchSingleWallet(userWallet: UserWallet): CompletionResult<Unit> {
return walletManagersRepository.findOrMakeSingleCurrencyWalletManager(
userWallet = userWallet,
refresh = refresh,
)
.flatMap { walletManager ->
val userWalletId = userWallet.walletId

View file

@ -17,7 +17,7 @@ internal class DummyWalletStoresManager : WalletStoresManager {
return emptyFlow()
}
override suspend fun delete(userWalletsIds: List<String>): CompletionResult<Unit> {
override suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
@ -32,4 +32,8 @@ internal class DummyWalletStoresManager : WalletStoresManager {
override suspend fun fetch(userWallets: List<UserWallet>, refresh: Boolean): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun updateAmounts(userWallets: List<UserWallet>): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
}

View file

@ -1,27 +0,0 @@
package com.tangem.tap.domain.walletStores.implementation.utils
import com.tangem.common.CompletionResult
internal fun List<CompletionResult<Unit>>.fold(): CompletionResult<Unit> {
return fold(Unit) { _, _ -> Unit }
}
@Suppress("UNCHECKED_CAST")
internal inline fun <reified D, reified R> List<CompletionResult<D>>.fold(
initial: R,
operation: (acc: R, data: D) -> R,
): CompletionResult<R> {
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<R>
}
}
}
return CompletionResult.Success(resultData)
}

View file

@ -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<UserWallet>,
fiatCurrency: FiatCurrency,
): CompletionResult<Unit>
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<Unit>
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<Unit>

View file

@ -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<WalletManager>
suspend fun findOrMakeSingleCurrencyWalletManager(userWallet: UserWallet): CompletionResult<WalletManager>
suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit>
suspend fun delete(

View file

@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.Flow
interface WalletStoresRepository {
fun getAll(): Flow<Map<UserWalletId, List<WalletStoreModel>>>
fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>>
suspend fun getSync(userWalletId: UserWalletId): List<WalletStoreModel>
suspend fun contains(userWalletId: UserWalletId): Boolean

View file

@ -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<UserWallet>,
fiatCurrency: FiatCurrency,
): CompletionResult<Unit> {
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<Unit> {
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<Unit> = 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<UserWallet>,
): CompletionResult<Unit> = 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<Unit> = withContext(Dispatchers.Default) {
userWallets.map { async { fetchAmountsForUserWallet(it) } }
.awaitAll()
.fold()
}
@ -120,7 +106,11 @@ internal class DefaultWalletAmountsRepository(
): CompletionResult<Unit> {
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<Unit> = 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<Unit> {
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<Unit> {
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<WalletStoreModel>,
fiatRates: Map<String, Double>,
) = 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)
},
)
}
}
}
}

View file

@ -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<WalletManager> {
return findOrMakeInternal(userWallet, blockchainNetwork)
}
override suspend fun findOrMakeSingleCurrencyWalletManager(userWallet: UserWallet): CompletionResult<WalletManager> {
return findOrMakeInternal(userWallet, blockchainNetwork = null)
}
private suspend fun findOrMakeInternal(
userWallet: UserWallet,
blockchainNetwork: BlockchainNetwork?,
refresh: Boolean,
): CompletionResult<WalletManager> = 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 }
}

View file

@ -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<List<WalletStoreModel>> {
return walletStoresStorage.get(userWalletId)
return getAll().map { it[userWalletId].orEmpty() }
}
override suspend fun getSync(userWalletId: UserWalletId): List<WalletStoreModel> {
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<UserWalletId>): CompletionResult<Unit> = catching {
@ -39,11 +45,13 @@ internal class DefaultWalletStoresRepository : WalletStoresRepository {
userWalletId: UserWalletId,
currentBlockchains: List<Blockchain>,
): CompletionResult<Unit> = 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<UserWalletId, List<WalletStoreModel>> = 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) },
)
}

View file

@ -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,

View file

@ -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<UserWalletId, List<WalletStoreModel>>.replaceWalletStore(
walletId: UserWalletId,
walletStore: WalletStoreModel,
walletStoreToUpdate: WalletStoreModel,
update: (walletStore: WalletStoreModel) -> WalletStoreModel,
): HashMap<UserWalletId, List<WalletStoreModel>> {
return replaceWalletStores(listOf(walletStoreToUpdate), update)
}
internal inline fun HashMap<UserWalletId, List<WalletStoreModel>>.replaceWalletStores(
walletStoresToUpdate: List<WalletStoreModel>,
update: (walletStore: WalletStoreModel) -> WalletStoreModel,
): HashMap<UserWalletId, List<WalletStoreModel>> {
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<WalletStoreModel>.replaceWalletStore(
newWalletStore: WalletStoreModel,
update: (walletStore: WalletStoreModel) -> WalletStoreModel,
): List<WalletStoreModel> {
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<WalletStoreModel>.replaceWalletStores(
walletStoresToUpdate: List<WalletStoreModel>,
update: (walletStore: WalletStoreModel) -> WalletStoreModel,
): List<WalletStoreModel> {
val mutableStores = ArrayList<WalletStoreModel>(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
}

View file

@ -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<UserWalletId, List<WalletManager>> {
return managers.first()
fun getAll(): SharedFlow<Map<UserWalletId, List<WalletManager>>> {
return managers.asSharedFlow()
}
private val mutex = Mutex()

View file

@ -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<Map<UserWalletId, List<WalletStoreModel>>> {
return stores
}
@OptIn(ExperimentalCoroutinesApi::class)
fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>> {
return stores
.mapLatest { stores ->
stores[userWalletId].orEmpty()
}
}
suspend fun getSync(userWalletId: UserWalletId): List<WalletStoreModel> {
return stores.first().getOrElse(userWalletId) { emptyList() }
fun getAll(): SharedFlow<Map<UserWalletId, List<WalletStoreModel>>> {
return stores.asSharedFlow()
}
private val mutex = Mutex()

View file

@ -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,
),
)
}
}
}
}

View file

@ -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,
)
}

View file

@ -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,

View file

@ -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()
}

View file

@ -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())
}
}
}

View file

@ -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
}

View file

@ -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(

View file

@ -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)
}
}

View file

@ -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))
}
}

View file

@ -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<String>? = 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(

View file

@ -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))
}
}
}
}

View file

@ -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<TwinCardsState>() {
}
}
override fun onStart() {
super.onStart()
setStatusBarColor(R.color.backgroundWhite)
}
private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) =
with(containerBinding) {
imvFrontCard.hide()

View file

@ -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))

View file

@ -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)))

View file

@ -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) {

View file

@ -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<TokenWithBlockchain>, blockchains: List<Blockchain> ->
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)
}

View file

@ -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) {

View file

@ -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<Currency>? = null,
) : WalletAction() {
data class Success(
val fiatRates: Map<Currency, BigDecimal?>
val fiatRates: Map<Currency, BigDecimal?>,
) : WalletAction()
object Failure : WalletAction()
@ -163,7 +164,7 @@ sealed class WalletAction : Action {
object ChooseTradeActionDialog : DialogAction()
data class ChooseCurrency(val amounts: List<Amount>?) : 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<WalletStoreModel>) : WalletAction()
data class TotalFiatBalanceChanged(val balance: TotalFiatBalance) : WalletAction()
data class WalletStoresChanged(val walletStores: List<WalletStoreModel>) : WalletAction() {
data class UpdateWalletStores(val reduxWalletStores: List<WalletStore>) : WalletAction()
}
data class TotalFiatBalanceChanged(val balance: TotalBalance) : WalletAction()
}

View file

@ -36,7 +36,7 @@ data class WalletState(
val cardImage: Artwork? = null,
val hashesCountVerified: Boolean? = null,
val mainWarningsList: List<WarningMessage> = mutableListOf(),
val wallets: List<WalletStore> = listOf(),
val walletsStores: List<WalletStore> = 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<WalletData>
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<Any, TwinCardsState> { _, _ ->
@ -63,20 +69,17 @@ data class WalletState(
get() = store.state.globalState.exchangeManager.featureIsSwitchedOn()
val blockchains: List<Blockchain>
get() = wallets.mapNotNull { it.walletManager?.wallet?.blockchain }
get() = walletsStores.mapNotNull { it.walletManager?.wallet?.blockchain }
val currencies: List<Currency>
get() = wallets.flatMap { it.walletsData }.map { it.currency }
val walletsData: List<WalletData>
get() = wallets.flatMap { it.walletsData }
get() = walletsStores.flatMap { it.walletsData }.map { it.currency }
val walletManagers: List<WalletManager>
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<WalletStore> {
if (wallet == null) return wallets
fun replaceWalletStoreInWalletsStores(wallet: WalletStore?): List<WalletStore> {
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<WalletData>
): WalletState {
fun updateWalletsData(walletsData: List<WalletData>): 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<WalletStore>): WalletState {
private fun updateWalletsStores(walletStores: List<WalletStore>): 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<WalletData>): List<WalletData> {
val remainingWallets: MutableList<WalletData> = 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<WalletData>.replaceSomeWalletsData(newWallets: List<WalletData>): List<WalletData> {
val remainingWallets: MutableList<WalletData> = 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<AddressData>
val list: List<AddressData>,
)
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<WalletWarning>){
private fun assembleTokenWarnings(walletWarnings: MutableList<WalletWarning>) {
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<WalletData>
val walletsData: List<WalletData>,
) {
fun updateWallets(walletDataList: List<WalletData>): WalletStore {
val relevantWalletDataList = walletDataList.filter {

View file

@ -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<WalletStoreModel>.mapToReduxModels(
isMultiWalletAllowed: Boolean,
): List<WalletStore> {
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<WalletDataModel>.mapToReduxModel(
isMultiWalletAllowed: Boolean,
walletRent: WalletStoreModel.WalletRent?,
): List<WalletData> {
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,
),
)
}
}
}

View file

@ -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))
}
}

View file

@ -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)))

View file

@ -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)

View file

@ -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<WalletStoreModel>, 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<WalletStoreModel>, 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))

View file

@ -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()

View file

@ -36,7 +36,7 @@ class MultiWalletReducer {
fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState {
return when (action) {
is WalletAction.MultiWallet.AddBlockchains -> {
val wallets: List<WalletStore> = action.blockchains.map { blockchain ->
val walletStores: List<WalletStore> = 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,
)
}

View file

@ -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) {

View file

@ -36,6 +36,7 @@ private fun List<WalletData>.mapToProgressState(): List<ProgressState> {
BalanceStatus.Unreachable,
BalanceStatus.EmptyCard,
BalanceStatus.UnknownBlockchain,
BalanceStatus.MissedDerivation,
-> ProgressState.Error
BalanceStatus.Loading,
null,

View file

@ -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<WalletStoreModel>.mapToReduxModel(
isMultiWalletAllowed: Boolean,
): List<WalletStore> {
return this.map { walletStoreModel ->
with(walletStoreModel) {
WalletStore(
walletManager = walletManager,
blockchainNetwork = blockchainNetwork,
walletsData = walletsData.mapToReduxModel(isMultiWalletAllowed, walletStoreModel.walletRent),
)
}
}
}
@JvmName("walletDataModelToReduxModel")
private fun List<WalletDataModel>.mapToReduxModel(
isMultiWalletAllowed: Boolean,
walletRent: WalletStoreModel.WalletRent?,
): List<WalletData> {
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

View file

@ -16,7 +16,8 @@ enum class BalanceStatus {
Refreshing,
NoAccount,
EmptyCard,
UnknownBlockchain
UnknownBlockchain,
MissedDerivation,
}
data class BalanceWidgetData(

View file

@ -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> {
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> {
(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
}

View file

@ -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<Walle
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
viewModel.launch()
}
override fun onStart() {
super.onStart()
setStatusBarColor(R.color.background_secondary)
store.subscribe(this) { state ->
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<Walle
walletView.removeFragment()
}
override fun onDestroy() {
walletView.onDestroyFragment()
super.onDestroy()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
@ -166,7 +175,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
binding.srlWallet.isRefreshing = state.state == ProgressState.Refreshing
binding.srlWallet.setOnRefreshListener {
if (state.state != ProgressState.Loading ||
if (state.state != ProgressState.Loading &&
state.state != ProgressState.Refreshing
) {
Analytics.send(Portfolio.Refreshed())

View file

@ -62,6 +62,9 @@ class WalletAdapter : ListAdapter<WalletData, WalletAdapter.WalletsViewHolder>(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<WalletData, WalletAdapter.WalletsViewHolder>(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)

View file

@ -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())
}
}

View file

@ -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<WalletState> {
val totalBalanceStrategy: DiffStrategy<WalletState> = { 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

View file

@ -52,4 +52,4 @@ class SaltPaySingleWalletView : WalletView() {
),
).setup()
}
}
}

View file

@ -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 {

View file

@ -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)

View file

@ -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,

View file

@ -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<String>,
val userWalletsIds: List<UserWalletId>,
) : WalletSelectorAction
object AddWallet : WalletSelectorAction {

View file

@ -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<UserWalletId, List<WalletStoreModel>>, 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<UserWallet> {
private suspend fun unlockUserWalletWithScannedCard(userWallet: UserWallet): CompletionResult<Unit> {
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<Unit> {
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<String>, state: WalletSelectorState) {
private fun deleteWallets(userWalletsIds: List<UserWalletId>, 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<Unit> {
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<UserWalletId>,
currentSelectedWalletId: UserWalletId?,
): CompletionResult<Unit> {
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<Unit> {
return userWalletsListManager.clear()
.flatMap { walletStoresManager.clear() }
.doOnSuccess {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
}
private suspend fun deleteAccessCodes(userWalletsIds: List<UserWalletId>): CompletionResult<Unit> {
val cardsIds = userWalletsListManager.userWallets.firstOrNull().orEmpty()
.asSequence()
.filter { it.walletId in userWalletsIds }
.flatMap { it.cardsInWallet }
private suspend fun removeUserWallets(
walletIdsToRemove: List<String>,
state: WalletSelectorState,
): CompletionResult<Unit> {
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(

View file

@ -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<UserWallet>.updateWalletsModels(prevWallets: List<UserWalletModel>): List<UserWalletModel> {
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<UserWalletModel> {
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()

View file

@ -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<UserWalletModel> = emptyList(),
val selectedWalletId: String? = null,
val selectedWalletId: UserWalletId? = null,
val isLocked: Boolean = false,
val fiatCurrency: FiatCurrency = FiatCurrency.Default,
val isCardSavingInProgress: Boolean = false,

View file

@ -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<UserWalletModel>.toUiModels(
imageUrl = artworkUrl,
balance = balance,
isLocked = isLocked,
tokenName = type.blockchainName ?: "",
tokenName = type.blockchainName,
)
}
}

View file

@ -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<Wa
@OptIn(ExperimentalComposeUiApi::class)
@Composable
override fun ScreenContent(
modifier: Modifier,
state: WalletSelectorScreenState,
) {
override fun ScreenContent(modifier: Modifier, state: WalletSelectorScreenState) {
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
val renameWalletDialog by rememberUpdatedState(newValue = state.renameWalletDialog)
Box(
modifier = modifier
.nestedScroll(connection = rememberNestedScrollInteropConnection()),
) {
Box(modifier = modifier.nestedScroll(rememberNestedScrollInteropConnection())) {
WalletSelectorScreenContent(
state = state,
onWalletClick = viewModel::walletClicked,
@ -82,10 +82,7 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
}
@Composable
private fun RenameWalletDialog(
modifier: Modifier = Modifier,
dialog: RenameWalletDialog?,
) {
private fun RenameWalletDialog(modifier: Modifier = Modifier, dialog: RenameWalletDialog?) {
if (dialog == null) return
RenameWalletDialogContent(modifier, dialog)
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.walletSelector.ui
import androidx.compose.runtime.Immutable
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.RenameWalletDialog
@ -10,9 +11,9 @@ import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletI
internal data class WalletSelectorScreenState(
val multiCurrencyWallets: List<MultiCurrencyUserWalletItem> = emptyList(),
val singleCurrencyWallets: List<SingleCurrencyUserWalletItem> = emptyList(),
val selectedWalletId: String? = null,
val selectedUserWalletId: UserWalletId? = null,
val isLocked: Boolean = false,
val editingWalletsIds: List<String> = listOf(),
val editingUserWalletsIds: List<UserWalletId> = listOf(),
val renameWalletDialog: RenameWalletDialog? = null,
val showAddCardProgress: Boolean = false,
val showUnlockProgress: Boolean = false,

View file

@ -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<WalletSele
store.dispatch(WalletSelectorAction.AddWallet)
}
fun walletClicked(walletId: String) = with(state.value) {
fun walletClicked(userWalletId: UserWalletId) = with(state.value) {
when {
isLocked -> {
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<WalletSele
stateInternal.update { prevState ->
prevState.copy(
renameWalletDialog = null,
editingWalletsIds = emptyList(),
editingUserWalletsIds = emptyList(),
)
}
},
@ -103,8 +102,8 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
}
fun deleteWallets() = with(state.value) {
if (editingWalletsIds.isNotEmpty()) {
store.dispatch(WalletSelectorAction.RemoveWallets(walletIdsToRemove = editingWalletsIds))
if (editingUserWalletsIds.isNotEmpty()) {
store.dispatch(WalletSelectorAction.RemoveWallets(userWalletsIds = editingUserWalletsIds))
}
}
@ -122,22 +121,27 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
store.unsubscribe(this)
}
private fun editWallet(walletId: String) {
private fun editWallet(userWalletId: UserWalletId) {
stateInternal.update { prevState ->
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 }

View file

@ -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,
)
}

View file

@ -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<String>,
editingWalletsIds: List<UserWalletId>,
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<UserWalletItem>,
singleCurrencyWallets: List<UserWalletItem>,
selectedWalletId: String?,
checkedWalletIds: List<String>,
onWalletClick: (walletId: String) -> Unit,
onWalletLongClick: (walletId: String) -> Unit,
) {
Column(modifier = modifier) {
val walletsSection = @Composable { wallets: List<UserWalletItem> ->
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 */ },

Some files were not shown because too many files have changed in this diff Show more