Updated on 2026-08-14
This commit is contained in:
parent
d1798a2c49
commit
6dee2eeed7
112 changed files with 2146 additions and 1562 deletions
|
|
@ -198,6 +198,7 @@ dependencies {
|
|||
implementation(Library.armadillo)
|
||||
implementation(Library.googlePlayServicesWallet)
|
||||
implementation(Library.composeShimmer)
|
||||
implementation(Library.mviCoreWatcher)
|
||||
|
||||
/** Testing libraries */
|
||||
testImplementation(Test.junit)
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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="
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -93,6 +93,7 @@ data class AppState(
|
|||
SaveWalletMiddleware().middleware,
|
||||
WalletSelectorMiddleware().middleware,
|
||||
LockUserWalletsTimerMiddleware().middleware,
|
||||
AccessCodeRequestPolicyMiddleware().middleware,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 -> {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,4 +17,5 @@ internal data class UserWalletPublicInformation(
|
|||
val artworkUrl: String,
|
||||
val cardsInWallet: Set<String>,
|
||||
val scanResponse: ScanResponse,
|
||||
val isMultiCurrency: Boolean,
|
||||
)
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -12,4 +12,6 @@ internal interface UserWalletsPublicInformationRepository {
|
|||
|
||||
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
|
||||
suspend fun clear(): CompletionResult<Unit>
|
||||
|
||||
fun isNotEmpty(): Boolean
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ private fun List<WalletData>.mapToProgressState(): List<ProgressState> {
|
|||
BalanceStatus.Unreachable,
|
||||
BalanceStatus.EmptyCard,
|
||||
BalanceStatus.UnknownBlockchain,
|
||||
BalanceStatus.MissedDerivation,
|
||||
-> ProgressState.Error
|
||||
BalanceStatus.Loading,
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ enum class BalanceStatus {
|
|||
Refreshing,
|
||||
NoAccount,
|
||||
EmptyCard,
|
||||
UnknownBlockchain
|
||||
UnknownBlockchain,
|
||||
MissedDerivation,
|
||||
}
|
||||
|
||||
data class BalanceWidgetData(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -52,4 +52,4 @@ class SaltPaySingleWalletView : WalletView() {
|
|||
),
|
||||
).setup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue