Updated on 2026-08-14
This commit is contained in:
commit
115645bccf
50 changed files with 950 additions and 511 deletions
|
|
@ -35,13 +35,10 @@ import com.tangem.tap.common.OnActivityResultCallback
|
|||
import com.tangem.tap.common.SnackbarHandler
|
||||
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
||||
import com.tangem.tap.common.redux.NotificationsHandler
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.shop.googlepay.GooglePayService
|
||||
import com.tangem.tap.common.shop.googlepay.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE
|
||||
import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
|
||||
import com.tangem.tap.domain.TangemSdkManager
|
||||
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
|
||||
import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
|
||||
import com.tangem.tap.features.intentHandler.IntentProcessor
|
||||
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
|
||||
|
|
@ -149,7 +146,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
backupService = BackupService.init(cardSdkConfigRepository.sdk, this)
|
||||
lockUserWalletsTimer = LockUserWalletsTimer(owner = this)
|
||||
|
||||
initUserWalletsListManager()
|
||||
initIntentHandlers()
|
||||
|
||||
store.dispatch(
|
||||
|
|
@ -251,18 +247,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
intentProcessor.addHandler(SellCurrencyIntentHandler())
|
||||
}
|
||||
|
||||
private fun initUserWalletsListManager() {
|
||||
val manager = if (preferencesStorage.shouldSaveUserWallets) {
|
||||
UserWalletsListManager.provideBiometricImplementation(
|
||||
context = applicationContext,
|
||||
tangemSdkManager = tangemSdkManager,
|
||||
)
|
||||
} else {
|
||||
UserWalletsListManager.provideRuntimeImplementation()
|
||||
}
|
||||
store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager))
|
||||
}
|
||||
|
||||
private fun updateAppTheme(appThemeMode: AppThemeMode) {
|
||||
MutableAppThemeModeHolder.value = appThemeMode
|
||||
MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme()
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import com.tangem.domain.common.LogConfig
|
|||
import com.tangem.domain.settings.repositories.AppRatingRepository
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.legacy.WalletManagersRepository
|
||||
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
|
||||
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
|
||||
|
|
@ -58,6 +59,8 @@ import com.tangem.tap.domain.tokens.UserTokensRepository
|
|||
import com.tangem.tap.domain.tokens.UserTokensStorageService
|
||||
import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
|
||||
import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation
|
||||
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
|
||||
import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation
|
||||
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
|
||||
import com.tangem.tap.domain.walletCurrencies.di.provideDefaultImplementation
|
||||
import com.tangem.tap.domain.walletStores.WalletStoresManager
|
||||
|
|
@ -252,6 +255,16 @@ internal class TapApplication : Application(), ImageLoaderFactory {
|
|||
walletConnectRepository = WalletConnectRepository(this)
|
||||
|
||||
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT)
|
||||
initUserWalletsListManager()
|
||||
|
||||
// TODO: Try to performance and user experience.
|
||||
// [REDACTED_JIRA]
|
||||
runBlocking {
|
||||
featureTogglesManager.init()
|
||||
appRatingRepository.initialize()
|
||||
// learn2earnInteractor.init()
|
||||
}
|
||||
|
||||
initConfigManager(configLoader, ::initWithConfigDependency)
|
||||
initWarningMessagesManager()
|
||||
|
||||
|
|
@ -279,14 +292,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
|
|||
appStateHolder.userTokensRepository = userTokensRepository
|
||||
appStateHolder.walletStoresManager = walletStoresManager
|
||||
|
||||
// TODO: Try to performance and user experience.
|
||||
// [REDACTED_JIRA]
|
||||
runBlocking {
|
||||
featureTogglesManager.init()
|
||||
appRatingRepository.initialize()
|
||||
// learn2earnInteractor.init()
|
||||
}
|
||||
|
||||
initTopUpController()
|
||||
walletConnect2Repository.init(projectId = configManager.config.walletConnectProjectId)
|
||||
}
|
||||
|
|
@ -350,14 +355,20 @@ internal class TapApplication : Application(), ImageLoaderFactory {
|
|||
foregroundActivityObserver: ForegroundActivityObserver,
|
||||
store: Store<AppState>,
|
||||
) {
|
||||
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply {
|
||||
appVersion = try {
|
||||
// TODO don't use deprecated method
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
pInfo.versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
"x.y.z"
|
||||
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo {
|
||||
return AdditionalFeedbackInfo(
|
||||
userWalletsListManager = userWalletsListManager,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
walletFeatureToggles = walletFeatureToggles,
|
||||
).apply {
|
||||
appVersion = try {
|
||||
// TODO don't use deprecated method
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
pInfo.versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
"x.y.z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -391,4 +402,14 @@ internal class TapApplication : Application(), ImageLoaderFactory {
|
|||
private fun initWarningMessagesManager() {
|
||||
store.dispatch(GlobalAction.SetWarningManager(WarningMessagesManager()))
|
||||
}
|
||||
|
||||
private fun initUserWalletsListManager() {
|
||||
val manager = if (preferencesStorage.shouldSaveUserWallets) {
|
||||
UserWalletsListManager.provideBiometricImplementation(applicationContext)
|
||||
} else {
|
||||
UserWalletsListManager.provideRuntimeImplementation()
|
||||
}
|
||||
|
||||
store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager))
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,36 @@ import com.tangem.blockchain.common.address.Address
|
|||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.userwallets.UserWalletIdBuilder
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.scope
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class AdditionalFeedbackInfo(
|
||||
userWalletsListManager: UserWalletsListManager,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
walletFeatureToggles: WalletFeatureToggles,
|
||||
) {
|
||||
|
||||
init {
|
||||
if (walletFeatureToggles.isRedesignedScreenEnabled) {
|
||||
userWalletsListManager.selectedUserWallet
|
||||
.distinctUntilChanged()
|
||||
.onEach { userWallet ->
|
||||
setCardInfo(data = userWallet.scanResponse)
|
||||
|
||||
walletManagersFacade.getAll(userWalletId = userWallet.walletId)
|
||||
.onEach(::setWalletsInfo)
|
||||
.launchIn(scope)
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
class AdditionalFeedbackInfo {
|
||||
class EmailWalletInfo(
|
||||
var blockchain: Blockchain = Blockchain.Unknown,
|
||||
var derivationPath: String = "",
|
||||
|
|
@ -46,6 +73,7 @@ class AdditionalFeedbackInfo {
|
|||
private val Address.name: String
|
||||
get() = type.javaClass.simpleName
|
||||
|
||||
@Deprecated("Don't use it directly")
|
||||
fun setCardInfo(data: ScanResponse) {
|
||||
cardId = data.card.cardId
|
||||
cardBlockchain = data.walletData?.blockchain ?: ""
|
||||
|
|
@ -55,6 +83,7 @@ class AdditionalFeedbackInfo {
|
|||
userWalletId = UserWalletIdBuilder.scanResponse(data).build()?.stringValue ?: ""
|
||||
}
|
||||
|
||||
@Deprecated("Don't use it directly")
|
||||
fun setWalletsInfo(walletManagers: List<WalletManager>) {
|
||||
walletsInfo.clear()
|
||||
tokens.clear()
|
||||
|
|
|
|||
|
|
@ -3,27 +3,28 @@ package com.tangem.tap.domain.userWalletList.di
|
|||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.common.authentication.AuthenticatedStorage
|
||||
import com.tangem.common.json.TangemSdkAdapter
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.sdk.storage.AndroidSecureStorage
|
||||
import com.tangem.sdk.storage.createEncryptedSharedPreferences
|
||||
import com.tangem.tap.domain.TangemSdkManager
|
||||
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
|
||||
import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager
|
||||
import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager
|
||||
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator
|
||||
import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
|
||||
import com.tangem.tap.domain.userWalletList.utils.json.*
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
|
||||
private const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage"
|
||||
|
||||
fun UserWalletsListManager.Companion.provideBiometricImplementation(
|
||||
context: Context,
|
||||
tangemSdkManager: TangemSdkManager,
|
||||
applicationContext: Context,
|
||||
): UserWalletsListManager {
|
||||
val moshi = Moshi.Builder()
|
||||
.add(WalletDerivedKeysMapAdapter())
|
||||
|
|
@ -40,7 +41,7 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation(
|
|||
|
||||
val secureStorage = AndroidSecureStorage(
|
||||
preferences = SecureStorage.createEncryptedSharedPreferences(
|
||||
context = context,
|
||||
context = applicationContext,
|
||||
storageName = USER_WALLETS_STORAGE_NAME,
|
||||
),
|
||||
)
|
||||
|
|
@ -48,16 +49,17 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation(
|
|||
val authenticatedStorage = AuthenticatedStorage(
|
||||
secureStorage = UserWalletsKeysStoreDecorator(
|
||||
featureStorage = secureStorage,
|
||||
cardSdkStorage = tangemSdkManager.secureStorage,
|
||||
cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage },
|
||||
),
|
||||
keystoreManager = DelegatedKeystoreManager(
|
||||
keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager },
|
||||
),
|
||||
keystoreManager = tangemSdkManager.keystoreManager,
|
||||
)
|
||||
|
||||
val keysRepository = BiometricUserWalletsKeysRepository(
|
||||
moshi = moshi,
|
||||
secureStorage = secureStorage,
|
||||
authenticatedStorage = authenticatedStorage,
|
||||
|
||||
)
|
||||
val publicInformationRepository = DefaultUserWalletsPublicInformationRepository(
|
||||
moshi = moshi,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.common.authentication.KeystoreManager
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
internal class DelegatedKeystoreManager(
|
||||
private val keystoreManagerProvider: Provider<KeystoreManager>,
|
||||
) : KeystoreManager {
|
||||
|
||||
override suspend fun authenticateAndGetKey(keyAlias: String): SecretKey? {
|
||||
return keystoreManagerProvider().authenticateAndGetKey(keyAlias)
|
||||
}
|
||||
|
||||
override suspend fun storeKey(keyAlias: String, key: SecretKey) {
|
||||
keystoreManagerProvider().storeKey(keyAlias, key)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,29 @@
|
|||
package com.tangem.tap.domain.userWalletList.repository
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.common.services.secure.SecureStorage
|
||||
|
||||
/**
|
||||
* A decorator for [SecureStorage] that facilitates data migration between two storages.
|
||||
*
|
||||
* @property featureStorage The primary storage, which will eventually contain all user data.
|
||||
* @property cardSdkStorage The SDK's storage where user data might have been previously stored.
|
||||
* @property cardSdkStorageProvider The SDK's storage where user data might have been previously stored.
|
||||
*/
|
||||
internal class UserWalletsKeysStoreDecorator(
|
||||
private val featureStorage: SecureStorage,
|
||||
private val cardSdkStorage: SecureStorage,
|
||||
private val cardSdkStorageProvider: Provider<SecureStorage>,
|
||||
) : SecureStorage by featureStorage {
|
||||
|
||||
override fun delete(account: String) {
|
||||
featureStorage.delete(account)
|
||||
cardSdkStorage.delete(account)
|
||||
cardSdkStorageProvider().delete(account)
|
||||
}
|
||||
|
||||
override fun get(account: String): ByteArray? {
|
||||
var data = featureStorage.get(account)
|
||||
|
||||
if (data == null) {
|
||||
data = cardSdkStorage.get(account) ?: return null
|
||||
data = cardSdkStorageProvider().get(account) ?: return null
|
||||
featureStorage.store(data, account)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.tangem.domain.common.extensions.*
|
|||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
|
||||
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
|
||||
|
|
@ -35,7 +36,6 @@ import com.tangem.tap.features.customtoken.impl.presentation.validators.ContactA
|
|||
import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidatorResult
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
|
|
@ -51,11 +51,11 @@ import javax.inject.Inject
|
|||
/**
|
||||
* ViewModel for add custom token screen
|
||||
*
|
||||
* @param analyticsEventHandler analytics event handler
|
||||
* @param featureRouter feature router
|
||||
* @property featureInteractor feature interactor
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
* @property reduxStateHolder redux state holder
|
||||
* @param analyticsEventHandler analytics event handler
|
||||
* @param featureRouter feature router
|
||||
* @property featureInteractor feature interactor
|
||||
* @property getSelectedWalletUseCase use case that returns selected wallet
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
|
@ -66,7 +66,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
featureRouter: CustomTokenRouter,
|
||||
private val featureInteractor: CustomTokenInteractor,
|
||||
private val dispatchers: AppCoroutineDispatcherProvider,
|
||||
private val reduxStateHolder: AppStateHolder,
|
||||
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
|
||||
private val analyticsSender = AddCustomTokenAnalyticsSender(analyticsEventHandler)
|
||||
|
|
@ -208,7 +208,10 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
|
||||
private fun getNetworkSelectorItems(): List<SelectorItem.Title> {
|
||||
val defaultNetwork = createNetworkSelectorItem(blockchain = Blockchain.Unknown)
|
||||
val scanResponse = reduxStateHolder.scanResponse
|
||||
val scanResponse = getSelectedWalletUseCase().fold(
|
||||
ifLeft = { null },
|
||||
ifRight = { it.scanResponse },
|
||||
)
|
||||
val derivationStyle = scanResponse?.derivationStyleProvider?.getDerivationStyle()
|
||||
return listOf(defaultNetwork) + Blockchain.values()
|
||||
.filter { blockchain ->
|
||||
|
|
@ -262,17 +265,20 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createDerivationPathsSelectorField(): AddCustomTokenSelectorField.DerivationPath? {
|
||||
val scanResponse = reduxStateHolder.scanResponse
|
||||
if (scanResponse?.card?.settings?.isHDWalletAllowed == false) return null
|
||||
return getSelectedWalletUseCase().fold(
|
||||
ifLeft = { null },
|
||||
ifRight = {
|
||||
if (!it.scanResponse.card.settings.isHDWalletAllowed) return null
|
||||
|
||||
val selectorItems =
|
||||
getDerivationPathsSelectorItems(scanResponse?.derivationStyleProvider)
|
||||
return AddCustomTokenSelectorField.DerivationPath(
|
||||
label = TextReference.Res(R.string.custom_token_derivation_path_input_title),
|
||||
selectedItem = requireNotNull(selectorItems.firstOrNull()),
|
||||
items = selectorItems,
|
||||
onMenuItemClick = actionsHandler::onDerivationPathSelectorItemClick,
|
||||
isEnabled = true,
|
||||
val selectorItems = getDerivationPathsSelectorItems(it.scanResponse.derivationStyleProvider)
|
||||
AddCustomTokenSelectorField.DerivationPath(
|
||||
label = TextReference.Res(R.string.custom_token_derivation_path_input_title),
|
||||
selectedItem = requireNotNull(selectorItems.firstOrNull()),
|
||||
items = selectorItems,
|
||||
onMenuItemClick = actionsHandler::onDerivationPathSelectorItemClick,
|
||||
isEnabled = true,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -315,14 +321,19 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createDerivationPathInputField(): AddCustomTokenInputField.DerivationPath? {
|
||||
if (reduxStateHolder.scanResponse?.card?.settings?.isHDWalletAllowed == false) return null
|
||||
return getSelectedWalletUseCase().fold(
|
||||
ifLeft = { null },
|
||||
ifRight = {
|
||||
if (!it.scanResponse.card.settings.isHDWalletAllowed) return null
|
||||
|
||||
return AddCustomTokenInputField.DerivationPath(
|
||||
value = "",
|
||||
onValueChange = actionsHandler::onDerivationPathValueChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
label = TextReference.Res(R.string.custom_token_custom_derivation),
|
||||
placeholder = TextReference.Str(value = DERIVATION_PATH_PLACEHOLDER),
|
||||
AddCustomTokenInputField.DerivationPath(
|
||||
value = "",
|
||||
onValueChange = actionsHandler::onDerivationPathValueChange,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
label = TextReference.Res(R.string.custom_token_custom_derivation),
|
||||
placeholder = TextReference.Str(value = DERIVATION_PATH_PLACEHOLDER),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -440,11 +451,15 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
val isSupportedToken = if (!isNetworkSelected()) {
|
||||
true
|
||||
} else {
|
||||
val scanResponse = reduxStateHolder.scanResponse
|
||||
scanResponse?.card?.canHandleToken(
|
||||
blockchain = networkSelectorValue,
|
||||
cardTypesResolver = scanResponse.cardTypesResolver,
|
||||
) ?: false
|
||||
getSelectedWalletUseCase().fold(
|
||||
ifLeft = { false },
|
||||
ifRight = {
|
||||
it.scanResponse.card.canHandleToken(
|
||||
blockchain = networkSelectorValue,
|
||||
cardTypesResolver = it.scanResponse.cardTypesResolver,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return buildSet {
|
||||
|
|
@ -478,13 +493,16 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
address = uiState.form.contractAddressInputField.value,
|
||||
blockchain = networkSelectorValue,
|
||||
)
|
||||
val scanResponse = reduxStateHolder.scanResponse
|
||||
val isSupportedToken = scanResponse?.card
|
||||
?.canHandleToken(
|
||||
blockchain = networkSelectorValue,
|
||||
cardTypesResolver = scanResponse.cardTypesResolver,
|
||||
)
|
||||
?: false
|
||||
|
||||
val isSupportedToken = getSelectedWalletUseCase().fold(
|
||||
ifLeft = { false },
|
||||
ifRight = {
|
||||
it.scanResponse.card.canHandleToken(
|
||||
blockchain = networkSelectorValue,
|
||||
cardTypesResolver = it.scanResponse.cardTypesResolver,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
uiState.copySealed(
|
||||
floatingButton = uiState.floatingButton.copy(
|
||||
|
|
@ -641,8 +659,12 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
private fun getDerivationPathForBlockchain(blockchain: Blockchain?): DerivationPath? {
|
||||
if (blockchain == null) return null
|
||||
|
||||
val derivationStyle = reduxStateHolder.scanResponse?.derivationStyleProvider?.getDerivationStyle()
|
||||
?: DerivationStyle.V1
|
||||
val derivationStyle = getSelectedWalletUseCase().fold(
|
||||
ifLeft = { null },
|
||||
ifRight = {
|
||||
it.scanResponse.derivationStyleProvider.getDerivationStyle()
|
||||
},
|
||||
)
|
||||
|
||||
val derivationNetwork = if (blockchain == Blockchain.Unknown) {
|
||||
uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
|
|
@ -653,12 +675,15 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun isUnsupportedBlockchain(blockchain: Blockchain): Boolean {
|
||||
val scanResponse = reduxStateHolder.scanResponse
|
||||
val canHandleToken = scanResponse?.card?.canHandleBlockchain(
|
||||
blockchain = blockchain,
|
||||
cardTypesResolver = scanResponse.cardTypesResolver,
|
||||
) ?: false
|
||||
return !canHandleToken
|
||||
return getSelectedWalletUseCase().fold(
|
||||
ifLeft = { false },
|
||||
ifRight = {
|
||||
!it.scanResponse.card.canHandleBlockchain(
|
||||
blockchain = blockchain,
|
||||
cardTypesResolver = it.scanResponse.cardTypesResolver,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private inner class ActionsHandler(private val featureRouter: CustomTokenRouter) {
|
||||
|
|
@ -827,7 +852,6 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onResetButtonClick() {
|
||||
val scanResponse = reduxStateHolder.scanResponse
|
||||
with(uiState.form) {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
|
|
|
|||
|
|
@ -74,8 +74,7 @@ sealed class DetailsAction : Action {
|
|||
}
|
||||
|
||||
data class CheckBiometricsStatus(
|
||||
val awaitStatusChange: Boolean,
|
||||
val lifecycleCoroutineScope: LifecycleCoroutineScope,
|
||||
val lifecycleScope: LifecycleCoroutineScope,
|
||||
) : AppSettings()
|
||||
|
||||
object EnrollBiometrics : AppSettings()
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
|||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -209,6 +210,9 @@ class DetailsMiddleware {
|
|||
}
|
||||
|
||||
class AppSettingsMiddleware {
|
||||
|
||||
private val checkBiometricsStatusJobHolder = JobHolder()
|
||||
|
||||
fun handle(state: DetailsState, action: DetailsAction.AppSettings) {
|
||||
when (action) {
|
||||
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
|
||||
|
|
@ -218,11 +222,7 @@ class DetailsMiddleware {
|
|||
}
|
||||
}
|
||||
is DetailsAction.AppSettings.CheckBiometricsStatus -> {
|
||||
checkBiometricsStatus(
|
||||
awaitStatusChange = action.awaitStatusChange,
|
||||
state = state,
|
||||
lifecycleScope = action.lifecycleCoroutineScope,
|
||||
)
|
||||
observeBiometricsStatusChanges(state, action.lifecycleScope)
|
||||
}
|
||||
is DetailsAction.AppSettings.EnrollBiometrics -> {
|
||||
enrollBiometrics()
|
||||
|
|
@ -245,27 +245,20 @@ class DetailsMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param awaitStatusChange If true then start a new coroutine and check the biometric status every 100
|
||||
* milliseconds until it changes
|
||||
* */
|
||||
private fun checkBiometricsStatus(
|
||||
awaitStatusChange: Boolean,
|
||||
state: DetailsState,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
) {
|
||||
lifecycleScope.launch {
|
||||
if (awaitStatusChange) {
|
||||
while (state.appSettingsState.needEnrollBiometrics == tangemSdkManager.needEnrollBiometrics) {
|
||||
delay(timeMillis = 100)
|
||||
private fun observeBiometricsStatusChanges(state: DetailsState, lifecycleScope: LifecycleCoroutineScope) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
do {
|
||||
val needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull()
|
||||
|
||||
if (needEnrollBiometrics != null &&
|
||||
needEnrollBiometrics != state.appSettingsState.needEnrollBiometrics
|
||||
) {
|
||||
store.dispatchWithMain(DetailsAction.AppSettings.BiometricsStatusChanged(needEnrollBiometrics))
|
||||
}
|
||||
}
|
||||
store.dispatchWithMain(
|
||||
DetailsAction.AppSettings.BiometricsStatusChanged(
|
||||
needEnrollBiometrics = tangemSdkManager.needEnrollBiometrics,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
delay(timeMillis = 500)
|
||||
} while (true)
|
||||
}.saveIn(checkBiometricsStatusJobHolder)
|
||||
}
|
||||
|
||||
private fun enrollBiometrics() {
|
||||
|
|
@ -454,10 +447,7 @@ class DetailsMiddleware {
|
|||
return null
|
||||
}
|
||||
|
||||
return UserWalletsListManager.provideBiometricImplementation(
|
||||
context = context,
|
||||
tangemSdkManager = tangemSdkManager,
|
||||
)
|
||||
return UserWalletsListManager.provideBiometricImplementation(context)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.tap.features.details.ui.appsettings
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -32,11 +31,6 @@ internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsS
|
|||
AppSettingsViewModel(store, detailsFeatureToggles, appCurrencyRepository)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel.checkBiometricsStatus(lifecycleScope)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
AppSettingsScreen(
|
||||
|
|
@ -49,6 +43,11 @@ internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsS
|
|||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.checkBiometricsStatus(lifecycleScope)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
store.subscribe(this) { state ->
|
||||
|
|
@ -58,11 +57,6 @@ internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsS
|
|||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refreshBiometricsStatus(lifecycleScope)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
store.unsubscribe(this)
|
||||
|
|
|
|||
|
|
@ -55,21 +55,7 @@ internal class AppSettingsViewModel(
|
|||
}
|
||||
|
||||
fun checkBiometricsStatus(lifecycleScope: LifecycleCoroutineScope) {
|
||||
store.dispatch(
|
||||
DetailsAction.AppSettings.CheckBiometricsStatus(
|
||||
awaitStatusChange = false,
|
||||
lifecycleCoroutineScope = lifecycleScope,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun refreshBiometricsStatus(lifecycleScope: LifecycleCoroutineScope) {
|
||||
store.dispatch(
|
||||
DetailsAction.AppSettings.CheckBiometricsStatus(
|
||||
awaitStatusChange = true,
|
||||
lifecycleCoroutineScope = lifecycleScope,
|
||||
),
|
||||
)
|
||||
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(lifecycleScope))
|
||||
}
|
||||
|
||||
private fun buildItems(state: AppSettingsState): ImmutableList<AppSettingsScreenState.Item> {
|
||||
|
|
|
|||
|
|
@ -140,10 +140,7 @@ internal class SaveWalletMiddleware {
|
|||
store.dispatchWithMain(SaveWalletAction.Save.Error(TangemSdkError.ExceptionError(error)))
|
||||
return
|
||||
}
|
||||
val manager = UserWalletsListManager.provideBiometricImplementation(
|
||||
context = context,
|
||||
tangemSdkManager = tangemSdkManager,
|
||||
)
|
||||
val manager = UserWalletsListManager.provideBiometricImplementation(context)
|
||||
|
||||
store.dispatchWithMain(GlobalAction.UpdateUserWalletsListManager(manager))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,26 +6,26 @@ import androidx.paging.PagingData
|
|||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.testnet.TestnetTokensStorage
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.tap.features.tokens.impl.domain.TokensListRepository
|
||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Default repository implementation of tokens list feature
|
||||
*
|
||||
* @property tangemTechApi Tangem Tech API
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
* @property reduxStateHolder redux state holder
|
||||
* @property testnetTokensStorage storage for getting testnet tokens data
|
||||
* @property tangemTechApi Tangem Tech API
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
* @property getSelectedWalletUseCase use case that returns selected wallet
|
||||
* @property testnetTokensStorage storage for getting testnet tokens data
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultTokensListRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val reduxStateHolder: AppStateHolder,
|
||||
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
private val testnetTokensStorage: TestnetTokensStorage,
|
||||
) : TokensListRepository {
|
||||
|
||||
|
|
@ -37,16 +37,23 @@ internal class DefaultTokensListRepository(
|
|||
enablePlaceholders = false,
|
||||
),
|
||||
pagingSourceFactory = {
|
||||
if (reduxStateHolder.scanResponse?.card?.isTestCard == true) {
|
||||
TestnetTokensPagingSource(testnetTokensStorage, searchText)
|
||||
} else {
|
||||
TangemApiTokensPagingSource(
|
||||
api = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
reduxStateHolder = reduxStateHolder,
|
||||
searchText = searchText,
|
||||
)
|
||||
}
|
||||
val defaultSource = TangemApiTokensPagingSource(
|
||||
api = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
getSelectedWalletUseCase = getSelectedWalletUseCase,
|
||||
searchText = searchText,
|
||||
)
|
||||
|
||||
getSelectedWalletUseCase().fold(
|
||||
ifLeft = { defaultSource },
|
||||
ifRight = {
|
||||
if (it.scanResponse.card.isTestCard) {
|
||||
TestnetTokensPagingSource(testnetTokensStorage, searchText)
|
||||
} else {
|
||||
defaultSource
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
).flow
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,24 +7,24 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
|
|||
import com.tangem.domain.common.extensions.supportedBlockchains
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.tap.features.tokens.impl.data.converters.CoinsResponseConverter
|
||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
|
||||
/**
|
||||
* Paging source that get tokens by Tangem Tech API
|
||||
*
|
||||
* @property api Tangem Tech API
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
* @property reduxStateHolder redux state holder
|
||||
* @property searchText search text
|
||||
* @property api Tangem Tech API
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
* @property getSelectedWalletUseCase use case that returns selected wallet
|
||||
* @property searchText search text
|
||||
*/
|
||||
internal class TangemApiTokensPagingSource(
|
||||
private val api: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val reduxStateHolder: AppStateHolder,
|
||||
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
private val searchText: String?,
|
||||
) : PagingSource<Int, Token>() {
|
||||
|
||||
|
|
@ -39,9 +39,10 @@ internal class TangemApiTokensPagingSource(
|
|||
val page = params.key ?: 0
|
||||
|
||||
return runCatching(dispatchers.io) {
|
||||
val scanResponse = reduxStateHolder.scanResponse
|
||||
val supportedBlockchains = scanResponse?.card?.supportedBlockchains(scanResponse.cardTypesResolver)
|
||||
?: Blockchain.values().toList()
|
||||
val supportedBlockchains = getSelectedWalletUseCase().fold(
|
||||
ifLeft = { Blockchain.values().toList() },
|
||||
ifRight = { it.scanResponse.card.supportedBlockchains(it.scanResponse.cardTypesResolver) },
|
||||
)
|
||||
|
||||
api.getCoins(
|
||||
networkIds = supportedBlockchains.joinToString(separator = ",", transform = Blockchain::toNetworkId),
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ package com.tangem.tap.features.tokens.impl.di
|
|||
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.testnet.TestnetTokensStorage
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.tap.features.tokens.impl.data.DefaultTokensListRepository
|
||||
import com.tangem.tap.features.tokens.impl.domain.DefaultTokensListInteractor
|
||||
import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -25,14 +25,14 @@ internal object TokensListInteractorModule {
|
|||
fun provideTokensListInteractor(
|
||||
tangemTechApi: TangemTechApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
reduxStateHolder: AppStateHolder,
|
||||
getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
testnetTokensStorage: TestnetTokensStorage,
|
||||
): TokensListInteractor {
|
||||
return DefaultTokensListInteractor(
|
||||
repository = DefaultTokensListRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
reduxStateHolder = reduxStateHolder,
|
||||
getSelectedWalletUseCase = getSelectedWalletUseCase,
|
||||
testnetTokensStorage = testnetTokensStorage,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ package com.tangem.tap.features.tokens.impl.di
|
|||
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.testnet.TestnetTokensStorage
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.tap.features.tokens.impl.data.DefaultTokensListRepository
|
||||
import com.tangem.tap.features.tokens.impl.domain.TokensListRepository
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -24,13 +24,13 @@ internal object TokensListRepositoryModule {
|
|||
fun providesTokensListRepository(
|
||||
tangemTechApi: TangemTechApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
reduxStateHolder: AppStateHolder,
|
||||
getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
testnetTokensStorage: TestnetTokensStorage,
|
||||
): TokensListRepository {
|
||||
return DefaultTokensListRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
reduxStateHolder = reduxStateHolder,
|
||||
getSelectedWalletUseCase = getSelectedWalletUseCase,
|
||||
testnetTokensStorage = testnetTokensStorage,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
|||
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.extensions.addOrReplace
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class DefaultWalletManagersStore(
|
||||
dataStore: StringKeyDataStore<List<WalletManager>>,
|
||||
|
|
@ -15,6 +16,10 @@ internal class DefaultWalletManagersStore(
|
|||
return key.stringValue
|
||||
}
|
||||
|
||||
override fun getAll(userWalletId: UserWalletId): Flow<List<WalletManager>> {
|
||||
return get(key = userWalletId)
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(
|
||||
userWalletId: UserWalletId,
|
||||
blockchain: Blockchain,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ package com.tangem.datasource.local.walletmanager
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface WalletManagersStore {
|
||||
|
||||
fun getAll(userWalletId: UserWalletId): Flow<List<WalletManager>>
|
||||
|
||||
suspend fun getSyncOrNull(
|
||||
userWalletId: UserWalletId,
|
||||
blockchain: Blockchain,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ fun ResizableText(
|
|||
fontSizeRange: FontSizeRange,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
) {
|
||||
val fontSizeValue = remember { mutableStateOf(fontSizeRange.max.value) }
|
||||
|
|
@ -33,12 +35,13 @@ fun ResizableText(
|
|||
}
|
||||
|
||||
Text(
|
||||
modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() },
|
||||
text = text,
|
||||
modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() },
|
||||
color = color,
|
||||
softWrap = false,
|
||||
style = style,
|
||||
fontSize = fontSizeValue.value.sp,
|
||||
overflow = overflow,
|
||||
softWrap = false,
|
||||
maxLines = maxLines,
|
||||
onTextLayout = {
|
||||
if (it.hasVisualOverflow) {
|
||||
val nextFontSizeValue = fontSizeValue.value - fontSizeRange.step.value
|
||||
|
|
@ -52,6 +55,7 @@ fun ResizableText(
|
|||
readyToDraw.value = true
|
||||
}
|
||||
},
|
||||
style = style,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ internal class DefaultCurrenciesRepository(
|
|||
derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider,
|
||||
)
|
||||
}
|
||||
.distinct()
|
||||
}
|
||||
|
||||
override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) =
|
||||
|
|
@ -186,6 +187,7 @@ internal class DefaultCurrenciesRepository(
|
|||
override suspend fun getMultiCurrencyWalletCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
id: CryptoCurrency.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency = withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
|
@ -194,10 +196,14 @@ internal class DefaultCurrenciesRepository(
|
|||
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
|
||||
responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse)
|
||||
responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse, derivationPath.value)
|
||||
}
|
||||
|
||||
override suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin {
|
||||
override suspend fun getNetworkCoin(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency.Coin {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true)
|
||||
|
||||
|
|
@ -207,15 +213,12 @@ internal class DefaultCurrenciesRepository(
|
|||
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
|
||||
}
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
val derivationPath = blockchain
|
||||
.derivationPath(userWallet.scanResponse.derivationStyleProvider.getDerivationStyle())
|
||||
?.rawPath
|
||||
val blockchainNetworkId = blockchain.toNetworkId()
|
||||
val coinId = blockchain.toCoinId()
|
||||
|
||||
val storedCoin = storedTokens.tokens
|
||||
.find {
|
||||
it.networkId == blockchain.toNetworkId() &&
|
||||
it.id == blockchain.toCoinId() &&
|
||||
it.derivationPath == derivationPath
|
||||
it.networkId == blockchainNetworkId && it.id == coinId && it.derivationPath == derivationPath.value
|
||||
} ?: error("Coin in this network $networkId not found")
|
||||
|
||||
val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse)
|
||||
|
|
@ -277,7 +280,7 @@ internal class DefaultCurrenciesRepository(
|
|||
)
|
||||
|
||||
userTokensStore.store(userWallet.walletId, response)
|
||||
fetchUserMarketCoinsByIds(userWalletId, response)
|
||||
fetchExchangeableUserMarketCoinsByIds(userWalletId, response)
|
||||
}
|
||||
|
||||
private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) {
|
||||
|
|
@ -285,10 +288,15 @@ internal class DefaultCurrenciesRepository(
|
|||
tangemTechApi.saveUserTokens(userWalletId.stringValue, response)
|
||||
}
|
||||
|
||||
private suspend fun fetchUserMarketCoinsByIds(userWalletId: UserWalletId, userTokens: UserTokensResponse) {
|
||||
private suspend fun fetchExchangeableUserMarketCoinsByIds(
|
||||
userWalletId: UserWalletId,
|
||||
userTokens: UserTokensResponse,
|
||||
) {
|
||||
try {
|
||||
val networkIds = userTokens.tokens.joinToString(separator = ",") { it.networkId }
|
||||
val response = tangemTechApi.getCoins(networkIds = networkIds)
|
||||
val networkIds = userTokens.tokens
|
||||
.distinctBy { it.networkId }
|
||||
.joinToString(separator = ",") { it.networkId }
|
||||
val response = tangemTechApi.getCoins(networkIds = networkIds, exchangeable = true)
|
||||
|
||||
userMarketCoinsStore.store(userWalletId, response)
|
||||
} catch (e: Throwable) {
|
||||
|
|
|
|||
|
|
@ -20,13 +20,10 @@ internal class ResponseCryptoCurrenciesFactory(private val demoConfig: DemoConfi
|
|||
currencyId: CryptoCurrency.ID,
|
||||
response: UserTokensResponse,
|
||||
scanResponse: ScanResponse,
|
||||
derivationPath: String?,
|
||||
): CryptoCurrency {
|
||||
val responseTokenId = currencyId.rawCurrencyId
|
||||
val blockchain = Blockchain.fromId(currencyId.rawNetworkId)
|
||||
val networkId = blockchain.toNetworkId()
|
||||
val derivationPath = blockchain
|
||||
.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle())
|
||||
?.rawPath
|
||||
val networkId = Blockchain.fromId(currencyId.rawNetworkId).toNetworkId()
|
||||
|
||||
val token = requireNotNull(
|
||||
value = response.tokens
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ internal class TangemCardTypesResolver(
|
|||
}
|
||||
|
||||
override fun isWhiteWallet(): Boolean {
|
||||
return walletData == null && card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
|
||||
return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable
|
||||
}
|
||||
|
||||
override fun isWallet2(): Boolean {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
|
|
@ -26,6 +26,7 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
|
|||
import com.tangem.domain.walletmanager.utils.*
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -275,6 +276,10 @@ class DefaultWalletManagersFacade(
|
|||
return if (manager is ExistentialDepositProvider) manager.getExistentialDeposit() else null
|
||||
}
|
||||
|
||||
override fun getAll(userWalletId: UserWalletId): Flow<List<WalletManager>> {
|
||||
return walletManagersStore.getAll(userWalletId)
|
||||
}
|
||||
|
||||
private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set<CryptoCurrency.Token>) {
|
||||
if (tokens.isEmpty()) return
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,16 @@ import com.tangem.blockchain.blockchains.solana.RentProvider
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.domain.txhistory.models.PaginationWrapper
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.models.TxHistoryState
|
||||
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.math.BigDecimal
|
||||
|
||||
// TODO: Move to its own module
|
||||
|
|
@ -97,4 +98,6 @@ interface WalletManagersFacade {
|
|||
* deactivated and any remaining funds will be destroyed.
|
||||
*/
|
||||
suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal?
|
||||
|
||||
fun getAll(userWalletId: UserWalletId): Flow<List<WalletManager>>
|
||||
}
|
||||
|
|
@ -36,16 +36,18 @@ class FetchCurrencyStatusUseCase(
|
|||
*
|
||||
* @param userWalletId The ID of the user's wallet.
|
||||
* @param id The ID of the cryptocurrency.
|
||||
* @param derivationPath currency derivation path.
|
||||
* @param refresh Indicates whether to force a refresh of the status data.
|
||||
* @return An [Either] representing success (Right) or an error (Left) in fetching the status.
|
||||
*/
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
id: CryptoCurrency.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
refresh: Boolean = false,
|
||||
): Either<CurrencyStatusError, Unit> {
|
||||
return either {
|
||||
val currency = getCurrency(userWalletId, id)
|
||||
val currency = getCurrency(userWalletId, id, derivationPath)
|
||||
|
||||
fetchCurrencyStatus(userWalletId, currency, refresh)
|
||||
}
|
||||
|
|
@ -87,8 +89,9 @@ class FetchCurrencyStatusUseCase(
|
|||
private suspend fun Raise<CurrencyStatusError>.getCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
id: CryptoCurrency.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency {
|
||||
return catch({ currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id) }) {
|
||||
return catch({ currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id, derivationPath) }) {
|
||||
raise(CurrencyStatusError.DataError(it))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import arrow.core.raise.catch
|
|||
import arrow.core.raise.either
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
|
|
@ -18,13 +19,15 @@ class GetCryptoCurrencyUseCase(
|
|||
*
|
||||
* @param userWalletId The ID of the user's wallet.
|
||||
* @param id The ID of the cryptocurrency.
|
||||
* @param derivationPath currency derivation path.
|
||||
* @return An [Either] representing success (Right) or an error (Left) in fetching the status.
|
||||
*/
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
id: CryptoCurrency.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): Either<CurrencyStatusError, CryptoCurrency> {
|
||||
return either { getCurrency(userWalletId, id) }
|
||||
return either { getCurrency(userWalletId, id, derivationPath) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -40,9 +43,10 @@ class GetCryptoCurrencyUseCase(
|
|||
private suspend fun Raise<CurrencyStatusError>.getCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
id: CryptoCurrency.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency {
|
||||
return catch(
|
||||
block = { currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id) },
|
||||
block = { currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id, derivationPath) },
|
||||
catch = { raise(CurrencyStatusError.DataError(it)) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.domain.tokens.error.CurrencyStatusError
|
|||
import com.tangem.domain.tokens.error.mapper.mapToCurrencyError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
|
|
@ -32,20 +33,23 @@ class GetCurrencyStatusUpdatesUseCase(
|
|||
*
|
||||
* @param userWalletId The unique identifier of the user's wallet.
|
||||
* @param currencyId The unique identifier of the cryptocurrency.
|
||||
* @param derivationPath currency derivation path.
|
||||
* @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation.
|
||||
*/
|
||||
operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
currencyId: CryptoCurrency.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
|
||||
return flow {
|
||||
emitAll(getCurrency(userWalletId, currencyId))
|
||||
emitAll(getCurrency(userWalletId, currencyId, derivationPath))
|
||||
}.flowOn(dispatchers.io)
|
||||
}
|
||||
|
||||
private suspend fun getCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
currencyId: CryptoCurrency.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
|
||||
val operations = CurrenciesStatusesOperations(
|
||||
currenciesRepository = currenciesRepository,
|
||||
|
|
@ -54,7 +58,7 @@ class GetCurrencyStatusUpdatesUseCase(
|
|||
userWalletId = userWalletId,
|
||||
)
|
||||
|
||||
return operations.getCurrencyStatusFlow(currencyId).map { maybeCurrency ->
|
||||
return operations.getCurrencyStatusFlow(currencyId, derivationPath).map { maybeCurrency ->
|
||||
maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,12 +24,14 @@ class GetCurrencyWarningsUseCase(
|
|||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): Flow<Set<CryptoCurrencyWarning>> {
|
||||
return combine(
|
||||
getFeeWarningFlow(
|
||||
userWalletId = userWalletId,
|
||||
networkId = currency.network.id,
|
||||
currencyId = currency.id,
|
||||
derivationPath = derivationPath,
|
||||
),
|
||||
flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)),
|
||||
flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)),
|
||||
|
|
@ -51,6 +53,7 @@ class GetCurrencyWarningsUseCase(
|
|||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
currencyId: CryptoCurrency.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): Flow<CryptoCurrencyWarning?> {
|
||||
val operations = CurrenciesStatusesOperations(
|
||||
currenciesRepository = currenciesRepository,
|
||||
|
|
@ -60,8 +63,8 @@ class GetCurrencyWarningsUseCase(
|
|||
)
|
||||
|
||||
return combine(
|
||||
operations.getCurrencyStatusFlow(currencyId).map { it.getOrNull() },
|
||||
operations.getNetworkCoinFlow(networkId).map { it.getOrNull() },
|
||||
operations.getCurrencyStatusFlow(currencyId, derivationPath).map { it.getOrNull() },
|
||||
operations.getNetworkCoinFlow(networkId, derivationPath).map { it.getOrNull() },
|
||||
) { tokenStatus, coinStatus ->
|
||||
when {
|
||||
tokenStatus != null && coinStatus != null -> {
|
||||
|
|
|
|||
|
|
@ -23,12 +23,14 @@ class GetNetworkCoinStatusUseCase(
|
|||
operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
|
||||
return flow {
|
||||
emitAll(
|
||||
flow = getCurrency(
|
||||
userWalletId = userWalletId,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -38,6 +40,7 @@ class GetNetworkCoinStatusUseCase(
|
|||
private suspend fun getCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
|
||||
val operations = CurrenciesStatusesOperations(
|
||||
currenciesRepository = currenciesRepository,
|
||||
|
|
@ -46,7 +49,7 @@ class GetNetworkCoinStatusUseCase(
|
|||
userWalletId = userWalletId,
|
||||
)
|
||||
|
||||
return operations.getNetworkCoinFlow(networkId).map { maybeCurrency ->
|
||||
return operations.getNetworkCoinFlow(networkId, derivationPath).map { maybeCurrency ->
|
||||
maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,18 +67,24 @@ internal class CurrenciesStatusesOperations(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun getCurrencyStatusFlow(currencyId: CryptoCurrency.ID): Flow<Either<Error, CryptoCurrencyStatus>> {
|
||||
suspend fun getCurrencyStatusFlow(
|
||||
currencyId: CryptoCurrency.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): Flow<Either<Error, CryptoCurrencyStatus>> {
|
||||
val currency = recover(
|
||||
block = { getMultiCurrencyWalletCurrency(currencyId) },
|
||||
block = { getMultiCurrencyWalletCurrency(currencyId, derivationPath) },
|
||||
recover = { return flowOf(it.left()) },
|
||||
)
|
||||
|
||||
return getCurrencyStatusFlow(currency)
|
||||
}
|
||||
|
||||
suspend fun getNetworkCoinFlow(networkId: Network.ID): Flow<Either<Error, CryptoCurrencyStatus>> {
|
||||
suspend fun getNetworkCoinFlow(
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): Flow<Either<Error, CryptoCurrencyStatus>> {
|
||||
val currency = recover(
|
||||
block = { getNetworkCoin(networkId) },
|
||||
block = { getNetworkCoin(networkId, derivationPath) },
|
||||
recover = { return flowOf(it.left()) },
|
||||
)
|
||||
|
||||
|
|
@ -178,14 +184,26 @@ internal class CurrenciesStatusesOperations(
|
|||
.onEmpty { emit(Error.EmptyCurrencies.left()) }
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.getMultiCurrencyWalletCurrency(currencyId: CryptoCurrency.ID): CryptoCurrency {
|
||||
return Either.catch { currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, currencyId) }
|
||||
private suspend fun Raise<Error>.getMultiCurrencyWalletCurrency(
|
||||
currencyId: CryptoCurrency.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency {
|
||||
return Either.catch {
|
||||
currenciesRepository.getMultiCurrencyWalletCurrency(
|
||||
userWalletId,
|
||||
currencyId,
|
||||
derivationPath,
|
||||
)
|
||||
}
|
||||
.mapLeft { Error.DataError(it) }
|
||||
.bind()
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.getNetworkCoin(networkId: Network.ID): CryptoCurrency {
|
||||
return Either.catch { currenciesRepository.getNetworkCoin(userWalletId, networkId) }
|
||||
private suspend fun Raise<Error>.getNetworkCoin(
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency {
|
||||
return Either.catch { currenciesRepository.getNetworkCoin(userWalletId, networkId, derivationPath) }
|
||||
.mapLeft { Error.DataError(it) }
|
||||
.bind()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,19 +98,29 @@ interface CurrenciesRepository {
|
|||
*
|
||||
* @param userWalletId The unique identifier of the user wallet.
|
||||
* @param id The unique identifier of the cryptocurrency to be retrieved.
|
||||
* @param derivationPath currency derivation path.
|
||||
* @return The cryptocurrency associated with the user wallet and ID.
|
||||
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
|
||||
* ID provided.
|
||||
*/
|
||||
suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: CryptoCurrency.ID): CryptoCurrency
|
||||
suspend fun getMultiCurrencyWalletCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
id: CryptoCurrency.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency
|
||||
|
||||
/**
|
||||
* Get the coin for a specific network.
|
||||
*
|
||||
* @param userWalletId The unique identifier of the user wallet.
|
||||
* @param networkId The unique identifier of the network.
|
||||
* @param derivationPath currency derivation path.
|
||||
*/
|
||||
suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin
|
||||
suspend fun getNetworkCoin(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency.Coin
|
||||
|
||||
/**
|
||||
* Determines whether the tokens within a specific multi-currency user wallet are grouped.
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ internal class MockCurrenciesRepository(
|
|||
override suspend fun getMultiCurrencyWalletCurrency(
|
||||
userWalletId: UserWalletId,
|
||||
id: CryptoCurrency.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency {
|
||||
val token = token.getOrElse { e -> throw e }
|
||||
|
||||
|
|
@ -75,7 +76,11 @@ internal class MockCurrenciesRepository(
|
|||
return token
|
||||
}
|
||||
|
||||
override suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin {
|
||||
override suspend fun getNetworkCoin(
|
||||
userWalletId: UserWalletId,
|
||||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency.Coin {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.domain.txhistory.repository.TxHistoryRepository
|
|||
|
||||
class GetTxHistoryItemsCountUseCase(private val repository: TxHistoryRepository) {
|
||||
|
||||
// FIXME: Provide UserWalletId
|
||||
suspend operator fun invoke(network: Network): Either<TxHistoryStateError, Int> {
|
||||
return either {
|
||||
catch(
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ private const val DEFAULT_PAGE_SIZE = 50
|
|||
|
||||
class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) {
|
||||
|
||||
// FIXME: Provide UserWalletId
|
||||
operator fun invoke(
|
||||
currency: CryptoCurrency,
|
||||
pageSize: Int = DEFAULT_PAGE_SIZE,
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ internal class TokenDetailsSkeletonStateConverter(
|
|||
pullToRefreshConfig = createPullToRefresh(),
|
||||
bottomSheetConfig = null,
|
||||
isBalanceHidden = true,
|
||||
isCustomToken = value.isCustom,
|
||||
isCustomToken = value is CryptoCurrency.Token && value.isCustom,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
|||
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
|
@ -23,6 +24,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.t
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
internal class TokenDetailsStateFactory(
|
||||
private val currentStateProvider: Provider<TokenDetailsState>,
|
||||
|
|
@ -89,6 +91,16 @@ internal class TokenDetailsStateFactory(
|
|||
return tokenDetailsButtonsConverter.convert(actions)
|
||||
}
|
||||
|
||||
fun getLoadingTxHistoryState(): TokenDetailsState {
|
||||
return currentStateProvider().copy(
|
||||
txHistoryState = TxHistoryState.Content(
|
||||
contentItems = MutableStateFlow(
|
||||
value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getLoadingTxHistoryState(itemsCountEither: Either<TxHistoryStateError, Int>): TokenDetailsState {
|
||||
return loadingTransactionsStateConverter.convert(value = itemsCountEither)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.domain.txhistory.models.TxHistoryStateError
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class TokenDetailsLoadingTxHistoryConverter(
|
||||
|
|
@ -34,22 +35,28 @@ internal class TokenDetailsLoadingTxHistoryConverter(
|
|||
|
||||
private fun convert(value: Int): TokenDetailsState {
|
||||
val state = currentStateProvider()
|
||||
val txHistoryContent = state.txHistoryState as TxHistoryState.Content
|
||||
|
||||
txHistoryContent.contentItems.update {
|
||||
PagingData.from(
|
||||
data = listOf(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) +
|
||||
MutableList(
|
||||
size = value,
|
||||
init = {
|
||||
TxHistoryState.TxHistoryItemState.Transaction(
|
||||
state = TransactionState.Loading(it.toString()),
|
||||
)
|
||||
},
|
||||
),
|
||||
return if (state.txHistoryState is TxHistoryState.Content) {
|
||||
state.txHistoryState.contentItems.update {
|
||||
PagingData.from(data = createLoadingItems(value))
|
||||
}
|
||||
state
|
||||
} else {
|
||||
val txHistoryContent = TxHistoryState.Content(
|
||||
contentItems = MutableStateFlow(
|
||||
value = PagingData.from(data = createLoadingItems(value)),
|
||||
),
|
||||
)
|
||||
state.copy(txHistoryState = txHistoryContent)
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
private fun createLoadingItems(size: Int): List<TxHistoryState.TxHistoryItemState> {
|
||||
return buildList {
|
||||
add(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick))
|
||||
(1..size).forEach {
|
||||
add(TxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading(it.toString())))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,10 +15,7 @@ import com.tangem.utils.extensions.isToday
|
|||
import com.tangem.utils.extensions.isYesterday
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
|
||||
|
|
@ -37,8 +34,12 @@ internal class TokenDetailsTxHistoryItemFlowConverter(
|
|||
}
|
||||
|
||||
override fun convert(value: Flow<PagingData<TxHistoryItem>>): TxHistoryState {
|
||||
val txHistoryContent = currentStateProvider().txHistoryState as TxHistoryState.Content
|
||||
|
||||
val state = currentStateProvider()
|
||||
val txHistoryContent = if (state.txHistoryState is TxHistoryState.Content) {
|
||||
state.txHistoryState
|
||||
} else {
|
||||
TxHistoryState.Content(contentItems = MutableStateFlow(PagingData.empty()))
|
||||
}
|
||||
// FIXME: TxHistoryRepository should send loading transactions
|
||||
// [REDACTED_JIRA]
|
||||
value
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import arrow.core.getOrElse
|
|||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
|
||||
|
|
@ -35,6 +36,8 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
|
@ -103,7 +106,7 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
|
||||
private fun updateContent(selectedWallet: UserWallet) {
|
||||
updateMarketPrice(selectedWallet = selectedWallet)
|
||||
updateTxHistory()
|
||||
updateTxHistory(refresh = false, showItemsLoading = true)
|
||||
updateWarnings(selectedWallet = selectedWallet)
|
||||
}
|
||||
|
||||
|
|
@ -137,6 +140,7 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
getCurrencyWarningsUseCase.invoke(
|
||||
userWalletId = selectedWallet.walletId,
|
||||
currency = cryptoCurrency,
|
||||
derivationPath = cryptoCurrency.network.derivationPath,
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
.onEach { uiState = stateFactory.getStateWithNotifications(it) }
|
||||
|
|
@ -148,6 +152,7 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
getCurrencyStatusUpdatesUseCase(
|
||||
userWalletId = selectedWallet.walletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
derivationPath = cryptoCurrency.network.derivationPath,
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
.onEach { either ->
|
||||
|
|
@ -162,13 +167,19 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
.saveIn(marketPriceJobHolder)
|
||||
}
|
||||
|
||||
private fun updateTxHistory(refresh: Boolean = false) {
|
||||
/**
|
||||
* @param refresh - invalidate cache and get data from remote
|
||||
* @param showItemsLoading - show loading items placeholder.
|
||||
*/
|
||||
@Suppress("UnusedPrivateMember") // will be removed after implement caching
|
||||
private fun updateTxHistory(refresh: Boolean, showItemsLoading: Boolean) {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
|
||||
if (!refresh) {
|
||||
// if countEither is left, handling error state run inside getLoadingTxHistoryState
|
||||
if (showItemsLoading || txHistoryItemsCountEither.isLeft()) {
|
||||
uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither)
|
||||
}
|
||||
|
||||
|
|
@ -211,7 +222,8 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
|
||||
override fun onReloadClick() {
|
||||
analyticsEventsHandler.send(TokenScreenEvent.ButtonReload(cryptoCurrency.symbol))
|
||||
updateTxHistory()
|
||||
uiState = stateFactory.getLoadingTxHistoryState()
|
||||
updateTxHistory(refresh = true, showItemsLoading = true)
|
||||
}
|
||||
|
||||
override fun onSendClick() {
|
||||
|
|
@ -237,6 +249,7 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
getNetworkCoinStatusUseCase(
|
||||
userWalletId = wallet.walletId,
|
||||
networkId = status.currency.network.id,
|
||||
derivationPath = status.currency.network.derivationPath,
|
||||
)
|
||||
.take(count = 1)
|
||||
.collectLatest {
|
||||
|
|
@ -358,13 +371,23 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
uiState = stateFactory.getRefreshingState()
|
||||
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
fetchCurrencyStatusUseCase.invoke(
|
||||
userWalletId = wallet.walletId,
|
||||
id = cryptoCurrency.id,
|
||||
refresh = true,
|
||||
)
|
||||
updateTxHistory(refresh = true)
|
||||
updateWarnings(wallet)
|
||||
listOf(
|
||||
async {
|
||||
fetchCurrencyStatusUseCase.invoke(
|
||||
userWalletId = wallet.walletId,
|
||||
id = cryptoCurrency.id,
|
||||
derivationPath = cryptoCurrency.network.derivationPath,
|
||||
refresh = true,
|
||||
)
|
||||
},
|
||||
async {
|
||||
updateTxHistory(
|
||||
refresh = true,
|
||||
showItemsLoading = uiState.txHistoryState !is TxHistoryState.Content,
|
||||
)
|
||||
},
|
||||
async { updateWarnings(wallet) },
|
||||
).awaitAll()
|
||||
uiState = stateFactory.getRefreshedState()
|
||||
}.saveIn(refreshStateJobHolder)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.common
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
|
|
@ -13,6 +12,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
|
||||
|
|
@ -37,10 +37,10 @@ internal object WalletPreviewData {
|
|||
val walletCardContentState by lazy {
|
||||
WalletCardState.Content(
|
||||
id = UserWalletId(stringValue = "123"),
|
||||
title = "Wallet 1",
|
||||
balance = "8923,05 $",
|
||||
additionalInfo = TextReference.Str("3 cards • Seed phrase"),
|
||||
imageResId = R.drawable.ill_businessman_3d,
|
||||
title = "Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1Wallet1",
|
||||
balance = "8923,05312312312312312312331231231233432423423424234 $",
|
||||
additionalInfo = TextReference.Str("3 cards • Seed phrase3 cards • Seed phraseцфвцфвфцвцфввцфвцф"),
|
||||
imageResId = R.drawable.ill_wallet2_cards3_120_106,
|
||||
onRenameClick = { _, _ -> },
|
||||
onDeleteClick = {},
|
||||
cardCount = 1,
|
||||
|
|
@ -51,7 +51,7 @@ internal object WalletPreviewData {
|
|||
WalletCardState.Loading(
|
||||
id = UserWalletId("321"),
|
||||
title = "Wallet 1",
|
||||
imageResId = R.drawable.ill_businessman_3d,
|
||||
imageResId = R.drawable.ill_wallet2_cards3_120_106,
|
||||
onRenameClick = { _, _ -> },
|
||||
onDeleteClick = {},
|
||||
)
|
||||
|
|
@ -61,7 +61,7 @@ internal object WalletPreviewData {
|
|||
WalletCardState.HiddenContent(
|
||||
id = UserWalletId("42"),
|
||||
title = "Wallet 1",
|
||||
imageResId = R.drawable.ill_businessman_3d,
|
||||
imageResId = R.drawable.ill_wallet2_cards3_120_106,
|
||||
onRenameClick = { _, _ -> },
|
||||
onDeleteClick = {},
|
||||
balance = "8923,05 $",
|
||||
|
|
@ -74,7 +74,7 @@ internal object WalletPreviewData {
|
|||
WalletCardState.Error(
|
||||
id = UserWalletId("24"),
|
||||
title = "Wallet 1",
|
||||
imageResId = R.drawable.ill_businessman_3d,
|
||||
imageResId = R.drawable.ill_wallet2_cards3_120_106,
|
||||
onRenameClick = { _, _ -> },
|
||||
onDeleteClick = {},
|
||||
)
|
||||
|
|
@ -167,6 +167,7 @@ internal object WalletPreviewData {
|
|||
iconState = tokenIconState,
|
||||
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
|
||||
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "3 172,14 $"),
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,19 +3,16 @@ package com.tangem.feature.wallet.presentation.common.component
|
|||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.layout.*
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.constraintlayout.compose.*
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.rememberHapticFeedback
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -24,152 +21,66 @@ import com.tangem.feature.wallet.presentation.common.component.token.*
|
|||
import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon
|
||||
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
|
||||
import org.burnoutcrew.reorderable.ReorderableLazyListState
|
||||
import kotlin.math.max
|
||||
|
||||
private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.22
|
||||
private const val PRICE_CHANGE_MIN_WIDTH_COEFFICIENT = 0.16
|
||||
|
||||
private enum class LayoutId {
|
||||
ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, PRICE_CHANGE, NON_FIAT_CONTENT
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun TokenItem(
|
||||
state: TokenItemState,
|
||||
modifier: Modifier = Modifier,
|
||||
reorderableTokenListState: ReorderableLazyListState? = null,
|
||||
) {
|
||||
var rootWidth by remember { mutableStateOf(Int.MIN_VALUE) }
|
||||
|
||||
@Suppress("DestructuringDeclarationWithTooManyEntries")
|
||||
BaseContainer(
|
||||
CustomContainer(
|
||||
state = state,
|
||||
modifier = modifier
|
||||
.tokenClickable(state)
|
||||
.onSizeChanged { rootWidth = it.width },
|
||||
.tokenClickable(state = state)
|
||||
.background(color = TangemTheme.colors.background.primary),
|
||||
) {
|
||||
val (iconRef, titleRef, cryptoAmountRef, fiatAmountRef, priceChangeRef, nonFiatContentRef) = createRefs()
|
||||
val isBalanceHidden = (state as? TokenItemState.Content)?.isBalanceHidden
|
||||
?: (state as? TokenItemState.Draggable)?.isBalanceHidden
|
||||
?: false
|
||||
|
||||
val isBalanceHidden = (state as? TokenItemState.Content)?.isBalanceHidden ?: false
|
||||
|
||||
TokenIcon(
|
||||
state = state.iconState,
|
||||
modifier = Modifier.constrainAs(iconRef) {
|
||||
centerVerticallyTo(parent)
|
||||
start.linkTo(parent.start)
|
||||
},
|
||||
)
|
||||
|
||||
val density = LocalDensity.current
|
||||
val titleRequiredMinWidth by remember(rootWidth) {
|
||||
derivedStateOf { with(density) { rootWidth.toDp().times(other = 0.22f) } }
|
||||
}
|
||||
TokenIcon(state = state.iconState, modifier = Modifier.layoutId(layoutId = LayoutId.ICON))
|
||||
|
||||
TokenTitle(
|
||||
state = state.titleState,
|
||||
modifier = Modifier
|
||||
.layoutId(layoutId = LayoutId.TITLE)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing8)
|
||||
.constrainAs(titleRef) {
|
||||
start.linkTo(iconRef.end)
|
||||
top.linkTo(parent.top)
|
||||
|
||||
width = Dimension.fillToConstraints.atLeast(dp = titleRequiredMinWidth)
|
||||
|
||||
when (state) {
|
||||
is TokenItemState.Content -> end.linkTo(fiatAmountRef.start)
|
||||
is TokenItemState.Draggable -> end.linkTo(nonFiatContentRef.start)
|
||||
is TokenItemState.Unreachable,
|
||||
is TokenItemState.NoAddress,
|
||||
-> {
|
||||
end.linkTo(nonFiatContentRef.start)
|
||||
bottom.linkTo(parent.bottom)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
},
|
||||
.padding(bottom = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
|
||||
TokenFiatAmount(
|
||||
state = state.fiatAmountState,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier.constrainAs(fiatAmountRef) {
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(parent.end)
|
||||
|
||||
width = Dimension.fillToConstraints.atMostWrapContent
|
||||
|
||||
if (state is TokenItemState.Content) {
|
||||
start.linkTo(titleRef.end)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.layoutId(layoutId = LayoutId.FIAT_AMOUNT)
|
||||
.padding(bottom = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
|
||||
val marginBetweenRows = TangemTheme.dimens.spacing2
|
||||
TokenCryptoAmount(
|
||||
state = state.cryptoAmountState,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing8)
|
||||
.constrainAs(cryptoAmountRef) {
|
||||
start.linkTo(iconRef.end)
|
||||
top.linkTo(titleRef.bottom, marginBetweenRows)
|
||||
bottom.linkTo(parent.bottom)
|
||||
|
||||
when (state) {
|
||||
is TokenItemState.Content -> {
|
||||
end.linkTo(priceChangeRef.start)
|
||||
width = Dimension.fillToConstraints.atMostWrapContent
|
||||
}
|
||||
is TokenItemState.Draggable -> {
|
||||
end.linkTo(nonFiatContentRef.start)
|
||||
width = Dimension.fillToConstraints
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
},
|
||||
.layoutId(layoutId = LayoutId.CRYPTO_AMOUNT)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
|
||||
val priceChangeRequiredMinWidth by remember(rootWidth) {
|
||||
derivedStateOf { with(density) { rootWidth.toDp().times(other = 0.16f) } }
|
||||
}
|
||||
TokenPriceChange(
|
||||
state = state.priceChangeState,
|
||||
modifier = Modifier.constrainAs(priceChangeRef) {
|
||||
top.linkTo(fiatAmountRef.bottom, marginBetweenRows)
|
||||
end.linkTo(anchor = parent.end)
|
||||
bottom.linkTo(parent.bottom)
|
||||
|
||||
when (state.priceChangeState) {
|
||||
is TokenItemState.PriceChangeState.Content,
|
||||
is TokenItemState.PriceChangeState.Unknown,
|
||||
-> {
|
||||
start.linkTo(cryptoAmountRef.end)
|
||||
width = Dimension.fillToConstraints
|
||||
.atLeast(priceChangeRequiredMinWidth)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
},
|
||||
modifier = Modifier.layoutId(layoutId = LayoutId.PRICE_CHANGE),
|
||||
)
|
||||
|
||||
NonFiatContentBlock(
|
||||
state = state,
|
||||
reorderableTokenListState = reorderableTokenListState,
|
||||
modifier = Modifier.constrainAs(nonFiatContentRef) {
|
||||
centerVerticallyTo(parent)
|
||||
end.linkTo(parent.end)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun BaseContainer(
|
||||
modifier: Modifier = Modifier,
|
||||
crossinline content: @Composable ConstraintLayoutScope.() -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size68)
|
||||
.background(color = TangemTheme.colors.background.primary),
|
||||
) {
|
||||
ConstraintLayout(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = TangemTheme.dimens.spacing14),
|
||||
content = content,
|
||||
modifier = Modifier.layoutId(layoutId = LayoutId.NON_FIAT_CONTENT),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -196,47 +107,299 @@ private fun Modifier.tokenClickable(state: TokenItemState): Modifier = composed
|
|||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
@Preview
|
||||
/**
|
||||
* IMPORTANT! All margins that used between children setup like as children paddings.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) {
|
||||
private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier, content: @Composable () -> Unit) {
|
||||
val density = LocalDensity.current
|
||||
val dimens = TangemTheme.dimens
|
||||
|
||||
Layout(content = content, modifier = modifier) { measurables, constraints ->
|
||||
|
||||
val layoutWidth = constraints.maxWidth
|
||||
val layoutPadding = with(density) { dimens.size14.roundToPx() }
|
||||
val layoutWidthWithPaddings = layoutWidth - 2 * layoutPadding
|
||||
|
||||
val titleMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt()
|
||||
val priceChangeMinWidth = (layoutWidth * PRICE_CHANGE_MIN_WIDTH_COEFFICIENT).toInt()
|
||||
|
||||
val icon = measurables.measure(layoutId = LayoutId.ICON, constraints = constraints)
|
||||
|
||||
/*
|
||||
* Title width take the whole REMAINING space.
|
||||
* If FiatAmount took the whole free space, then Title will has min width.
|
||||
*/
|
||||
val title: Placeable
|
||||
|
||||
// FiatAmount width must take the whole free space but is not greater the Title min size
|
||||
var fiatAmount: Placeable? = null
|
||||
|
||||
// CryptoAmount width must take the whole free space but is not greater the PriceChange min size
|
||||
var cryptoAmount: Placeable? = null
|
||||
|
||||
/*
|
||||
* PriceChange width take the whole REMAINING space.
|
||||
* If CryptoAmount took the whole free space, then PriceChange will has min width.
|
||||
*/
|
||||
val priceChange: Placeable?
|
||||
|
||||
val nonFiatContent = measurables.measure(layoutId = LayoutId.NON_FIAT_CONTENT, constraints = constraints)
|
||||
|
||||
var firstRowRemainingFreeSpace: Int? = null
|
||||
var secondRowRemainingFreeSpace: Int? = null
|
||||
|
||||
when (state) {
|
||||
is TokenItemState.Content,
|
||||
is TokenItemState.Loading,
|
||||
is TokenItemState.Locked,
|
||||
-> {
|
||||
fiatAmount = measurables.measureFiatAmount(
|
||||
state = state,
|
||||
maxWidth = layoutWidthWithPaddings - icon.width - titleMinWidth,
|
||||
defaultConstraints = constraints,
|
||||
)
|
||||
|
||||
cryptoAmount = measurables.measureCryptoAmount(
|
||||
state = state,
|
||||
maxWidth = layoutWidthWithPaddings - icon.width - priceChangeMinWidth,
|
||||
defaultConstraints = constraints,
|
||||
)
|
||||
|
||||
firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - fiatAmount.width
|
||||
secondRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - cryptoAmount.width
|
||||
}
|
||||
is TokenItemState.Draggable -> {
|
||||
cryptoAmount = measurables.measureCryptoAmount(
|
||||
state = state,
|
||||
maxWidth = layoutWidthWithPaddings - icon.width - nonFiatContent.width,
|
||||
defaultConstraints = constraints,
|
||||
)
|
||||
|
||||
firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - nonFiatContent.width
|
||||
}
|
||||
is TokenItemState.NoAddress,
|
||||
is TokenItemState.Unreachable,
|
||||
-> {
|
||||
firstRowRemainingFreeSpace = layoutWidthWithPaddings - icon.width - nonFiatContent.width
|
||||
}
|
||||
}
|
||||
|
||||
title = measurables.measureTitle(
|
||||
state = state,
|
||||
minWidth = titleMinWidth,
|
||||
remainingFreeSpace = firstRowRemainingFreeSpace,
|
||||
defaultConstraints = constraints,
|
||||
)
|
||||
|
||||
priceChange = secondRowRemainingFreeSpace?.let {
|
||||
measurables.measurePriceChange(
|
||||
state = state,
|
||||
minWidth = priceChangeMinWidth,
|
||||
remainingFreeSpace = secondRowRemainingFreeSpace,
|
||||
defaultConstraints = constraints,
|
||||
)
|
||||
}
|
||||
|
||||
val layoutHeight = calculateLayoutHeight(
|
||||
state = state,
|
||||
minLayoutHeight = with(density) { dimens.size68.roundToPx() },
|
||||
layoutPadding = layoutPadding,
|
||||
betweenRowsPadding = with(density) { dimens.size2.roundToPx() },
|
||||
title = title,
|
||||
fiatAmount = fiatAmount,
|
||||
cryptoAmount = cryptoAmount,
|
||||
priceChange = priceChange,
|
||||
)
|
||||
|
||||
layout(width = constraints.maxWidth, height = layoutHeight) {
|
||||
icon.placeRelative(x = layoutPadding, y = (layoutHeight - icon.height).div(other = 2))
|
||||
|
||||
title.placeRelative(
|
||||
x = layoutPadding + icon.width,
|
||||
y = when (state) {
|
||||
is TokenItemState.NoAddress,
|
||||
is TokenItemState.Unreachable,
|
||||
-> (layoutHeight - title.height).div(other = 2)
|
||||
else -> layoutPadding
|
||||
},
|
||||
)
|
||||
|
||||
cryptoAmount?.placeRelative(
|
||||
x = layoutPadding + icon.width,
|
||||
y = layoutHeight - cryptoAmount.height - layoutPadding,
|
||||
)
|
||||
|
||||
fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width - layoutPadding, y = layoutPadding)
|
||||
|
||||
priceChange?.placeRelative(
|
||||
x = layoutWidth - priceChange.width - layoutPadding,
|
||||
y = layoutHeight - priceChange.height - layoutPadding,
|
||||
)
|
||||
|
||||
nonFiatContent.placeRelative(
|
||||
x = layoutWidth - nonFiatContent.width - layoutPadding,
|
||||
y = (layoutHeight - nonFiatContent.height).div(other = 2),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Measurable>.measureFiatAmount(
|
||||
state: TokenItemState,
|
||||
maxWidth: Int,
|
||||
defaultConstraints: Constraints,
|
||||
): Placeable {
|
||||
return measure(
|
||||
layoutId = LayoutId.FIAT_AMOUNT,
|
||||
constraints = when (state) {
|
||||
is TokenItemState.Content,
|
||||
is TokenItemState.Draggable,
|
||||
-> createConstrainsSafely(maxWidth = maxWidth)
|
||||
else -> defaultConstraints
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<Measurable>.measureCryptoAmount(
|
||||
state: TokenItemState,
|
||||
maxWidth: Int,
|
||||
defaultConstraints: Constraints,
|
||||
): Placeable {
|
||||
return measure(
|
||||
layoutId = LayoutId.CRYPTO_AMOUNT,
|
||||
constraints = when (state) {
|
||||
is TokenItemState.Content,
|
||||
is TokenItemState.Draggable,
|
||||
-> createConstrainsSafely(maxWidth = maxWidth)
|
||||
else -> defaultConstraints
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<Measurable>.measureTitle(
|
||||
state: TokenItemState,
|
||||
minWidth: Int,
|
||||
remainingFreeSpace: Int,
|
||||
defaultConstraints: Constraints,
|
||||
): Placeable {
|
||||
return measure(
|
||||
layoutId = LayoutId.TITLE,
|
||||
constraints = when (state) {
|
||||
is TokenItemState.Content,
|
||||
is TokenItemState.Draggable,
|
||||
is TokenItemState.NoAddress,
|
||||
is TokenItemState.Unreachable,
|
||||
-> createDynamicConstrains(minWidth = minWidth, remainingFreeSpace = remainingFreeSpace)
|
||||
else -> defaultConstraints
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<Measurable>.measurePriceChange(
|
||||
state: TokenItemState,
|
||||
minWidth: Int,
|
||||
remainingFreeSpace: Int,
|
||||
defaultConstraints: Constraints,
|
||||
): Placeable {
|
||||
return measure(
|
||||
layoutId = LayoutId.PRICE_CHANGE,
|
||||
constraints = when (state) {
|
||||
is TokenItemState.Content,
|
||||
-> createDynamicConstrains(minWidth = minWidth, remainingFreeSpace = remainingFreeSpace)
|
||||
else -> defaultConstraints
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<Measurable>.measure(layoutId: LayoutId, constraints: Constraints): Placeable {
|
||||
return requireNotNull(
|
||||
value = firstOrNull { it.layoutId == layoutId },
|
||||
lazyMessage = { "Measurables[$layoutId] is null" },
|
||||
).measure(constraints)
|
||||
}
|
||||
|
||||
private fun createDynamicConstrains(minWidth: Int, remainingFreeSpace: Int): Constraints {
|
||||
return createConstrainsSafely(
|
||||
minWidth = minWidth,
|
||||
maxWidth = max(a = minWidth, b = remainingFreeSpace),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createConstrainsSafely(
|
||||
minWidth: Int = 0,
|
||||
maxWidth: Int = Constraints.Infinity,
|
||||
minHeight: Int = 0,
|
||||
maxHeight: Int = Constraints.Infinity,
|
||||
): Constraints {
|
||||
return Constraints(
|
||||
minWidth = minWidth.makeNotLessZero(),
|
||||
maxWidth = maxWidth.makeNotLessZero(),
|
||||
minHeight = minHeight.makeNotLessZero(),
|
||||
maxHeight = maxHeight.makeNotLessZero(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun Int.makeNotLessZero(): Int = max(a = 0, b = this)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun calculateLayoutHeight(
|
||||
state: TokenItemState,
|
||||
minLayoutHeight: Int,
|
||||
layoutPadding: Int,
|
||||
betweenRowsPadding: Int,
|
||||
title: Placeable,
|
||||
fiatAmount: Placeable?,
|
||||
cryptoAmount: Placeable?,
|
||||
priceChange: Placeable?,
|
||||
): Int {
|
||||
val firstColumnHeight: Int
|
||||
val secondColumnHeight: Int
|
||||
|
||||
when (state) {
|
||||
is TokenItemState.Content,
|
||||
is TokenItemState.Loading,
|
||||
is TokenItemState.Locked,
|
||||
-> {
|
||||
firstColumnHeight = 2 * layoutPadding + title.height + betweenRowsPadding + (cryptoAmount?.height ?: 0)
|
||||
secondColumnHeight = 2 * layoutPadding + (fiatAmount?.height ?: 0) + betweenRowsPadding +
|
||||
(priceChange?.height ?: 0)
|
||||
}
|
||||
is TokenItemState.Draggable,
|
||||
is TokenItemState.NoAddress,
|
||||
is TokenItemState.Unreachable,
|
||||
-> {
|
||||
firstColumnHeight = minLayoutHeight
|
||||
secondColumnHeight = minLayoutHeight
|
||||
}
|
||||
}
|
||||
|
||||
return max(firstColumnHeight, secondColumnHeight).coerceAtLeast(minLayoutHeight)
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360)
|
||||
@Composable
|
||||
private fun Preview_CustomTokenItem_InLight(@PreviewParameter(TokenItemStateProvider::class) state: TokenItemState) {
|
||||
TangemTheme(isDark = false) {
|
||||
TokenItem(state)
|
||||
TokenItem(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_Tokens_DarkTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) {
|
||||
TangemTheme(isDark = true) {
|
||||
TokenItem(state)
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenConfigProvider : CollectionPreviewParameterProvider<TokenItemState>(
|
||||
private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenItemState>(
|
||||
collection = listOf(
|
||||
WalletPreviewData.tokenItemVisibleState.copy(
|
||||
cryptoAmountState = TokenItemState.CryptoAmountState.Content(
|
||||
text = "5,41221467146712416241274127841274174213421 MATIC",
|
||||
),
|
||||
),
|
||||
WalletPreviewData.tokenItemVisibleState.copy(
|
||||
priceChangeState = TokenItemState.PriceChangeState.Content(
|
||||
valueInPercent = "31231231231231231231223123123123212312312312.0%",
|
||||
type = PriceChangeType.UP,
|
||||
),
|
||||
),
|
||||
WalletPreviewData.tokenItemVisibleState.copy(
|
||||
cryptoAmountState = TokenItemState.CryptoAmountState.Content(
|
||||
text = "5,41221467146712416241274127841274174213421 MATIC",
|
||||
),
|
||||
priceChangeState = TokenItemState.PriceChangeState.Content(
|
||||
valueInPercent = "31231231231231231231223123123123212312312312.0%",
|
||||
type = PriceChangeType.UP,
|
||||
),
|
||||
),
|
||||
WalletPreviewData.tokenItemVisibleState.copy(
|
||||
iconState = WalletPreviewData.coinIconState.copy(showCustomBadge = true),
|
||||
titleState = TokenItemState.TitleState.Content(
|
||||
text = "PolygonPolygonPolygonPolygonPolygonPolygon",
|
||||
hasPending = true,
|
||||
),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3213123123321312312312312312 $"),
|
||||
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,4123123213123123123123123123 MATIC"),
|
||||
priceChangeState = TokenItemState.PriceChangeState.Content(
|
||||
valueInPercent = "2365723643724723423742342374623642374723472342342.0%",
|
||||
type = PriceChangeType.UP,
|
||||
),
|
||||
),
|
||||
WalletPreviewData.tokenItemUnreachableState,
|
||||
WalletPreviewData.tokenItemNoAddressState,
|
||||
|
|
@ -247,6 +410,4 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider<TokenItem
|
|||
WalletPreviewData.customTokenItemVisibleState,
|
||||
WalletPreviewData.customTestnetTokenItemVisibleState,
|
||||
),
|
||||
)
|
||||
|
||||
// endregion preview
|
||||
)
|
||||
|
|
@ -40,6 +40,7 @@ private fun ContentTitle(name: String, hasPending: Boolean, modifier: Modifier =
|
|||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing6),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
/*
|
||||
* If currency name has a long width, then it will completely displace the image.
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ internal sealed class TokenItemState {
|
|||
override val id: String,
|
||||
override val iconState: IconState,
|
||||
override val titleState: TitleState,
|
||||
override val fiatAmountState: FiatAmountState?,
|
||||
override val fiatAmountState: FiatAmountState,
|
||||
override val cryptoAmountState: CryptoAmountState.Content,
|
||||
override val priceChangeState: PriceChangeState?,
|
||||
val isBalanceHidden: Boolean,
|
||||
|
|
@ -72,6 +72,7 @@ internal sealed class TokenItemState {
|
|||
override val iconState: IconState,
|
||||
override val titleState: TitleState,
|
||||
override val cryptoAmountState: CryptoAmountState,
|
||||
val isBalanceHidden: Boolean,
|
||||
) : TokenItemState() {
|
||||
override val fiatAmountState: FiatAmountState? = null
|
||||
override val priceChangeState: PriceChangeState? = null
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeToken
|
|||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverter
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListHiddenStateConverter
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListErrorConverter
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListSortingErrorConverter
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.CryptoCurrencyToDraggableItemConverter
|
||||
|
|
@ -24,12 +25,14 @@ internal class OrganizeTokensStateHolder(
|
|||
private val intents: OrganizeTokensIntents,
|
||||
private val dragAndDropIntents: DragAndDropIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val isBalanceHiddenProvider: Provider<Boolean>,
|
||||
private val listStateProvider: Provider<OrganizeTokensListState>,
|
||||
) {
|
||||
|
||||
private val stateFlowInternal: MutableStateFlow<OrganizeTokensState> = MutableStateFlow(getInitialState())
|
||||
|
||||
private val tokenListConverter by lazy {
|
||||
val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider)
|
||||
val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider, isBalanceHiddenProvider)
|
||||
val itemsConverter = TokenListToListStateConverter(
|
||||
tokensConverter = tokensConverter,
|
||||
groupsConverter = NetworkGroupToDraggableItemsConverter(tokensConverter),
|
||||
|
|
@ -38,9 +41,8 @@ internal class OrganizeTokensStateHolder(
|
|||
TokenListToStateConverter(Provider(stateFlowInternal::value), itemsConverter)
|
||||
}
|
||||
|
||||
private val inProgressStateConverter by lazy {
|
||||
InProgressStateConverter()
|
||||
}
|
||||
private val inProgressStateConverter by lazy { InProgressStateConverter() }
|
||||
private val tokenListHiddenStateConverter by lazy { TokenListHiddenStateConverter(listStateProvider) }
|
||||
|
||||
private val tokenListErrorConverter by lazy {
|
||||
TokenListErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter)
|
||||
|
|
@ -80,6 +82,10 @@ internal class OrganizeTokensStateHolder(
|
|||
updateState { copy(header = header.copy(isSortedByBalance = false)) }
|
||||
}
|
||||
|
||||
fun updateHiddenState(isBalanceHidden: Boolean) {
|
||||
updateState { copy(itemsState = tokenListHiddenStateConverter.convert(isBalanceHidden)) }
|
||||
}
|
||||
|
||||
fun updateStateWithError(error: TokenListError) {
|
||||
updateState { tokenListErrorConverter.convert(error) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
|
||||
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
|
||||
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
|
||||
import com.tangem.domain.tokens.GetTokenListUseCase
|
||||
import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase
|
||||
|
|
@ -36,6 +38,8 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase,
|
||||
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase,
|
||||
private val listenToFlipsUseCase: ListenToFlipsUseCase,
|
||||
private val analyticsEventsHandler: AnalyticsEventHandler,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
|
|
@ -45,6 +49,8 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
|
||||
private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow()
|
||||
|
||||
private var isBalanceHidden = true
|
||||
|
||||
private val dragAndDropAdapter = DragAndDropAdapter(
|
||||
listStateProvider = Provider { uiState.value.itemsState },
|
||||
)
|
||||
|
|
@ -53,6 +59,8 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
intents = this,
|
||||
dragAndDropIntents = dragAndDropAdapter,
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
isBalanceHiddenProvider = Provider { isBalanceHidden },
|
||||
listStateProvider = Provider { uiState.value.itemsState },
|
||||
)
|
||||
|
||||
private val userWalletId: UserWalletId by lazy {
|
||||
|
|
@ -68,6 +76,20 @@ internal class OrganizeTokensViewModel @Inject constructor(
|
|||
override fun onCreate(owner: LifecycleOwner) {
|
||||
analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened)
|
||||
|
||||
isBalanceHiddenUseCase()
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.onEach { hidden ->
|
||||
isBalanceHidden = hidden
|
||||
stateHolder.updateHiddenState(isBalanceHidden)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
viewModelScope.launch {
|
||||
listenToFlipsUseCase()
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.collect()
|
||||
}
|
||||
|
||||
bootstrapTokenList()
|
||||
bootstrapDragAndDropUpdates()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,4 +19,12 @@ internal sealed class OrganizeTokensListState {
|
|||
object Empty : OrganizeTokensListState() {
|
||||
override val items: PersistentList<DraggableItem> = persistentListOf()
|
||||
}
|
||||
|
||||
fun copySealed(items: PersistentList<DraggableItem> = this.items): OrganizeTokensListState {
|
||||
return when (this) {
|
||||
is GroupedByNetwork -> copy(items = items)
|
||||
is Ungrouped -> copy(items = items)
|
||||
is Empty -> Empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error
|
||||
|
||||
import com.tangem.common.Provider
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem
|
||||
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class TokenListHiddenStateConverter(
|
||||
private val currentStateProvider: Provider<OrganizeTokensListState>,
|
||||
) : Converter<Boolean, OrganizeTokensListState> {
|
||||
|
||||
override fun convert(input: Boolean): OrganizeTokensListState {
|
||||
val currentState = currentStateProvider()
|
||||
val isBalanceHidden = input
|
||||
|
||||
return currentState.copySealed(
|
||||
currentState.items.map { draggableItem ->
|
||||
if (draggableItem is DraggableItem.Token) {
|
||||
draggableItem.copy(
|
||||
tokenItemState = draggableItem.tokenItemState.copy(
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
draggableItem
|
||||
}
|
||||
}.toPersistentList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,26 +13,29 @@ import com.tangem.utils.converter.Converter
|
|||
|
||||
internal class CryptoCurrencyToDraggableItemConverter(
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val isBalanceHiddenProvider: Provider<Boolean>,
|
||||
) : Converter<CryptoCurrencyStatus, DraggableItem.Token> {
|
||||
|
||||
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
|
||||
|
||||
override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token {
|
||||
return createDraggableToken(value, appCurrencyProvider())
|
||||
return createDraggableToken(value, appCurrencyProvider(), isBalanceHiddenProvider())
|
||||
}
|
||||
|
||||
override fun convertList(input: Collection<CryptoCurrencyStatus>): List<DraggableItem.Token> {
|
||||
val appCurrency = appCurrencyProvider()
|
||||
val isBalanceHidden = isBalanceHiddenProvider()
|
||||
|
||||
return input.map { createDraggableToken(it, appCurrency) }
|
||||
return input.map { createDraggableToken(it, appCurrency, isBalanceHidden) }
|
||||
}
|
||||
|
||||
private fun createDraggableToken(
|
||||
currencyStatus: CryptoCurrencyStatus,
|
||||
appCurrency: AppCurrency,
|
||||
isBalanceHidden: Boolean,
|
||||
): DraggableItem.Token {
|
||||
return DraggableItem.Token(
|
||||
tokenItemState = createTokenItemState(currencyStatus, appCurrency),
|
||||
tokenItemState = createTokenItemState(currencyStatus, appCurrency, isBalanceHidden),
|
||||
groupId = getGroupHeaderId(currencyStatus.currency.network),
|
||||
)
|
||||
}
|
||||
|
|
@ -40,6 +43,7 @@ internal class CryptoCurrencyToDraggableItemConverter(
|
|||
private fun createTokenItemState(
|
||||
currencyStatus: CryptoCurrencyStatus,
|
||||
appCurrency: AppCurrency,
|
||||
isBalanceHidden: Boolean,
|
||||
): TokenItemState.Draggable {
|
||||
val currency = currencyStatus.currency
|
||||
|
||||
|
|
@ -52,6 +56,8 @@ internal class CryptoCurrencyToDraggableItemConverter(
|
|||
} else {
|
||||
TokenItemState.CryptoAmountState.Content(text = getFormattedFiatAmount(currencyStatus, appCurrency))
|
||||
},
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
|
|
@ -28,16 +27,13 @@ import androidx.compose.ui.platform.LocalDensity
|
|||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.ConstraintLayoutScope
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.constraintlayout.compose.*
|
||||
import com.tangem.common.Strings
|
||||
import com.tangem.core.ui.components.FontSizeRange
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.ResizableText
|
||||
|
|
@ -50,6 +46,8 @@ import com.tangem.feature.wallet.impl.R
|
|||
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
|
||||
|
||||
private const val HALF_OF_ITEM_WIDTH = 0.5
|
||||
|
||||
/**
|
||||
* Wallet card
|
||||
*
|
||||
|
|
@ -67,47 +65,65 @@ internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) {
|
|||
onRenameClick = { state.onRenameClick(state.id, it) },
|
||||
isLockedState = state is WalletCardState.LockedContent,
|
||||
modifier = modifier,
|
||||
) {
|
||||
val (title, balance, additionalText, image) = createRefs()
|
||||
) { itemSize ->
|
||||
val (titleRef, balanceRef, additionalTextRef, imageRef) = createRefs()
|
||||
|
||||
val contentVerticalMargin = TangemTheme.dimens.spacing12
|
||||
Title(
|
||||
state = state,
|
||||
modifier = Modifier.constrainAs(title) {
|
||||
TitleText(
|
||||
text = state.title,
|
||||
modifier = Modifier.constrainAs(titleRef) {
|
||||
start.linkTo(parent.start)
|
||||
top.linkTo(anchor = parent.top, margin = contentVerticalMargin)
|
||||
end.linkTo(image.start)
|
||||
end.linkTo(imageRef.start)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
|
||||
val betweenContentMargin = TangemTheme.dimens.spacing8
|
||||
var balanceWidth by remember { mutableStateOf(value = Int.MIN_VALUE) }
|
||||
Balance(
|
||||
state = state,
|
||||
modifier = Modifier.constrainAs(balance) {
|
||||
start.linkTo(parent.start)
|
||||
top.linkTo(anchor = title.bottom, margin = betweenContentMargin)
|
||||
bottom.linkTo(anchor = additionalText.top, margin = betweenContentMargin)
|
||||
},
|
||||
modifier = Modifier
|
||||
.onSizeChanged { balanceWidth = it.width }
|
||||
.padding(vertical = TangemTheme.dimens.spacing8)
|
||||
.constrainAs(balanceRef) {
|
||||
start.linkTo(parent.start)
|
||||
top.linkTo(anchor = titleRef.bottom)
|
||||
bottom.linkTo(anchor = additionalTextRef.top)
|
||||
},
|
||||
)
|
||||
|
||||
AdditionalInfo(
|
||||
state = state,
|
||||
modifier = Modifier.constrainAs(additionalText) {
|
||||
text = resolveAdditionalTextByState(state),
|
||||
modifier = Modifier.constrainAs(additionalTextRef) {
|
||||
start.linkTo(parent.start)
|
||||
top.linkTo(balanceRef.bottom)
|
||||
bottom.linkTo(anchor = parent.bottom, margin = contentVerticalMargin)
|
||||
|
||||
when (state) {
|
||||
is WalletCardState.Content,
|
||||
is WalletCardState.Error,
|
||||
is WalletCardState.HiddenContent,
|
||||
-> {
|
||||
end.linkTo(imageRef.start)
|
||||
width = Dimension.fillToConstraints
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
val imageWidth = TangemTheme.dimens.size120
|
||||
// If balance has a large width then image must be hidden
|
||||
val hasSpaceForImage by remember(key1 = balanceWidth, key2 = itemSize.width) {
|
||||
mutableStateOf(value = balanceWidth < itemSize.width * HALF_OF_ITEM_WIDTH)
|
||||
}
|
||||
|
||||
Image(
|
||||
id = state.imageResId,
|
||||
modifier = Modifier.constrainAs(image) {
|
||||
centerVerticallyTo(parent)
|
||||
top.linkTo(parent.top)
|
||||
isVisible = hasSpaceForImage,
|
||||
modifier = Modifier.constrainAs(imageRef) {
|
||||
end.linkTo(parent.end)
|
||||
bottom.linkTo(parent.bottom)
|
||||
height = Dimension.fillToConstraints
|
||||
width = Dimension.value(imageWidth)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -120,11 +136,11 @@ private fun CardContainer(
|
|||
onRenameClick: (String) -> Unit,
|
||||
isLockedState: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable (ConstraintLayoutScope.() -> Unit),
|
||||
content: @Composable (ConstraintLayoutScope.(IntSize) -> Unit),
|
||||
) {
|
||||
var isMenuVisible by rememberSaveable { mutableStateOf(value = false) }
|
||||
var pressOffset by remember { mutableStateOf(value = DpOffset.Zero) }
|
||||
var itemHeight by remember { mutableStateOf(value = 0.dp) }
|
||||
var itemSize by remember { mutableStateOf(value = IntSize.Zero) }
|
||||
|
||||
val density = LocalDensity.current
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
|
@ -138,7 +154,7 @@ private fun CardContainer(
|
|||
Modifier
|
||||
} else {
|
||||
Modifier
|
||||
.onSizeChanged { itemHeight = with(density) { it.height.toDp() } }
|
||||
.onSizeChanged { itemSize = it }
|
||||
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
|
||||
.indication(interactionSource = interactionSource, indication = LocalIndication.current)
|
||||
.pointerInput(true) {
|
||||
|
|
@ -166,12 +182,15 @@ private fun CardContainer(
|
|||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing14),
|
||||
) {
|
||||
content()
|
||||
content(itemSize)
|
||||
}
|
||||
}
|
||||
|
||||
var isRenameWalletDialogVisible by rememberSaveable { mutableStateOf(value = false) }
|
||||
|
||||
val itemHeight by remember(itemSize.height) {
|
||||
mutableStateOf(value = with(density) { itemSize.height.toDp() })
|
||||
}
|
||||
ManageWalletContextMenu(
|
||||
isMenuVisible = isMenuVisible,
|
||||
pressOffset = pressOffset,
|
||||
|
|
@ -243,22 +262,14 @@ private fun MenuItem(@StringRes textResId: Int, imageVector: ImageVector, onClic
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(state: WalletCardState, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
) {
|
||||
TitleText(title = state.title)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleText(title: String) {
|
||||
private fun TitleText(text: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = title,
|
||||
text = text,
|
||||
modifier = modifier,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.button,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TangemTheme.typography.button,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -269,6 +280,10 @@ private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) {
|
|||
targetState = state,
|
||||
label = "Update the balance",
|
||||
modifier = modifier,
|
||||
transitionSpec = {
|
||||
fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) with
|
||||
fadeOut(animationSpec = tween(durationMillis = 90))
|
||||
},
|
||||
) { walletCardState ->
|
||||
when (walletCardState) {
|
||||
is WalletCardState.Content -> {
|
||||
|
|
@ -277,10 +292,12 @@ private fun Balance(state: WalletCardState, modifier: Modifier = Modifier) {
|
|||
fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
|
||||
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.h2,
|
||||
)
|
||||
}
|
||||
is WalletCardState.HiddenContent -> NonContentBalanceText(text = WalletCardState.HIDDEN_BALANCE_TEXT)
|
||||
is WalletCardState.HiddenContent -> NonContentBalanceText(TextReference.Str(Strings.STARS))
|
||||
is WalletCardState.Error -> NonContentBalanceText(text = WalletCardState.EMPTY_BALANCE_TEXT)
|
||||
is WalletCardState.Loading -> {
|
||||
RectangleShimmer(modifier = Modifier.nonContentBalanceSize(TangemTheme.dimens))
|
||||
|
|
@ -309,29 +326,41 @@ private fun Modifier.nonContentBalanceSize(dimens: TangemDimens): Modifier {
|
|||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
private fun AdditionalInfo(state: WalletCardState, modifier: Modifier = Modifier) {
|
||||
private fun AdditionalInfo(text: TextReference?, modifier: Modifier = Modifier) {
|
||||
AnimatedContent(
|
||||
targetState = state,
|
||||
targetState = text,
|
||||
label = "Update the additional text",
|
||||
modifier = modifier,
|
||||
) { animatedState ->
|
||||
when (animatedState) {
|
||||
is WalletCardState.Content -> AdditionalInfoText(text = animatedState.additionalInfo)
|
||||
is WalletCardState.HiddenContent -> AdditionalInfoText(text = animatedState.additionalInfo)
|
||||
is WalletCardState.LockedContent -> AdditionalInfoText(text = animatedState.additionalInfo)
|
||||
is WalletCardState.Error -> AdditionalInfoText(text = WalletCardState.EMPTY_BALANCE_TEXT)
|
||||
is WalletCardState.Loading -> {
|
||||
RectangleShimmer(modifier = Modifier.nonContentAdditionalInfoSize(dimens = TangemTheme.dimens))
|
||||
}
|
||||
transitionSpec = {
|
||||
fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) with
|
||||
fadeOut(animationSpec = tween(durationMillis = 90))
|
||||
},
|
||||
) { animatedText ->
|
||||
if (animatedText != null) {
|
||||
AdditionalInfoText(text = animatedText)
|
||||
} else {
|
||||
RectangleShimmer(modifier = Modifier.nonContentAdditionalInfoSize(dimens = TangemTheme.dimens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveAdditionalTextByState(state: WalletCardState): TextReference? {
|
||||
return when (state) {
|
||||
is WalletCardState.Content -> state.additionalInfo
|
||||
is WalletCardState.LockedContent -> state.additionalInfo
|
||||
is WalletCardState.Error -> WalletCardState.EMPTY_BALANCE_TEXT
|
||||
is WalletCardState.HiddenContent -> WalletCardState.HIDDEN_BALANCE_TEXT
|
||||
is WalletCardState.Loading -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AdditionalInfoText(text: TextReference) {
|
||||
Text(
|
||||
text = text.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TangemTheme.typography.caption,
|
||||
)
|
||||
}
|
||||
|
|
@ -351,11 +380,14 @@ private fun LockedContent(modifier: Modifier = Modifier) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun Image(@DrawableRes id: Int?, modifier: Modifier = Modifier) {
|
||||
AnimatedVisibility(visible = id != null, modifier = modifier) {
|
||||
private fun Image(@DrawableRes id: Int?, isVisible: Boolean, modifier: Modifier = Modifier) {
|
||||
AnimatedVisibility(visible = id != null && isVisible, modifier = modifier) {
|
||||
val imageRes = id ?: return@AnimatedVisibility
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = requireNotNull(id)),
|
||||
painter = painterResource(id = imageRes),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.width(width = TangemTheme.dimens.size120),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -579,6 +579,7 @@ internal class WalletViewModel @Inject constructor(
|
|||
getNetworkCoinStatusUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
networkId = cryptoCurrencyStatus.currency.network.id,
|
||||
derivationPath = cryptoCurrencyStatus.currency.network.derivationPath,
|
||||
)
|
||||
.take(count = 1)
|
||||
.collectLatest {
|
||||
|
|
@ -913,8 +914,26 @@ internal class WalletViewModel @Inject constructor(
|
|||
|
||||
private fun getSingleCurrencyContent(index: Int) {
|
||||
val wallet = getWallet(index)
|
||||
updatePrimaryCurrencyStatus(userWalletId = wallet.walletId)
|
||||
updateNotifications(index)
|
||||
getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = wallet.walletId)
|
||||
.distinctUntilChanged()
|
||||
.onEach { maybeCryptoCurrencyStatus ->
|
||||
uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus)
|
||||
|
||||
maybeCryptoCurrencyStatus.onRight { status ->
|
||||
singleWalletCryptoCurrencyStatus = status
|
||||
|
||||
if (status.value.amount?.isZero() == false) {
|
||||
setWalletWithFundsFoundUseCase()
|
||||
}
|
||||
|
||||
updateNotifications(index)
|
||||
updateButtons(userWalletId = wallet.walletId, currencyStatus = status)
|
||||
updateTxHistory(status.currency)
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(marketPriceJobHolder)
|
||||
}
|
||||
|
||||
private fun updateTxHistory(currency: CryptoCurrency) {
|
||||
|
|
@ -935,28 +954,6 @@ internal class WalletViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun updatePrimaryCurrencyStatus(userWalletId: UserWalletId) {
|
||||
getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId)
|
||||
.distinctUntilChanged()
|
||||
.onEach { maybeCryptoCurrencyStatus ->
|
||||
uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus)
|
||||
|
||||
maybeCryptoCurrencyStatus.onRight { status ->
|
||||
singleWalletCryptoCurrencyStatus = status
|
||||
|
||||
if (status.value.amount?.isZero() == false) {
|
||||
setWalletWithFundsFoundUseCase()
|
||||
}
|
||||
|
||||
updateButtons(userWalletId = userWalletId, currencyStatus = status)
|
||||
updateTxHistory(status.currency)
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(marketPriceJobHolder)
|
||||
}
|
||||
|
||||
private fun updateButtons(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) {
|
||||
getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrencyStatus = currencyStatus)
|
||||
.distinctUntilChanged()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue