Updated on 2026-08-14
This commit is contained in:
commit
1d6b36877b
72 changed files with 1130 additions and 246 deletions
|
|
@ -74,6 +74,7 @@ dependencies {
|
|||
implementation(projects.data.tokens)
|
||||
implementation(projects.data.txhistory)
|
||||
implementation(projects.data.wallets)
|
||||
implementation(projects.data.analytics)
|
||||
|
||||
/** Features */
|
||||
implementation(projects.features.onboarding)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.blockchain.common.BlockchainSdkConfig
|
|||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.filter.OneTimeEventFilter
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import com.tangem.datasource.api.common.MoshiConverter
|
||||
|
|
@ -216,6 +217,9 @@ internal class TapApplication : Application(), ImageLoaderFactory {
|
|||
|
||||
@Inject
|
||||
lateinit var sendFeatureToggles: SendFeatureToggles
|
||||
|
||||
@Inject
|
||||
lateinit var oneTimeEventFilter: OneTimeEventFilter
|
||||
// endregion Injected
|
||||
|
||||
override fun onCreate() {
|
||||
|
|
@ -353,6 +357,8 @@ internal class TapApplication : Application(), ImageLoaderFactory {
|
|||
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder())
|
||||
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
|
||||
|
||||
factory.addFilter(oneTimeEventFilter)
|
||||
|
||||
val buildData = AnalyticsHandlerBuilder.Data(
|
||||
application = application,
|
||||
config = config,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ class AmplitudeAnalyticsHandler(
|
|||
|
||||
override fun id(): String = ID
|
||||
|
||||
override fun send(event: String, params: Map<String, String>) {
|
||||
client.logEvent(event, params)
|
||||
override fun send(eventId: String, params: Map<String, String>) {
|
||||
client.logEvent(eventId, params)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ class AppsFlyerAnalyticsHandler(
|
|||
|
||||
override fun id(): String = ID
|
||||
|
||||
override fun send(event: String, params: Map<String, String>) {
|
||||
client.logEvent(event, params)
|
||||
override fun send(eventId: String, params: Map<String, String>) {
|
||||
client.logEvent(eventId, params)
|
||||
}
|
||||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ class FirebaseAnalyticsHandler(
|
|||
|
||||
override fun id(): String = ID
|
||||
|
||||
override fun send(event: String, params: Map<String, String>) {
|
||||
client.logEvent(event, params)
|
||||
override fun send(eventId: String, params: Map<String, String>) {
|
||||
client.logEvent(eventId, params)
|
||||
}
|
||||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,7 @@ package com.tangem.tap.di.domain
|
|||
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.tokens.repository.*
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -231,4 +228,10 @@ internal object TokensDomainModule {
|
|||
): GetMissedAddressesCryptoCurrenciesUseCase {
|
||||
return GetMissedAddressesCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetGlobalTokenListUseCase(tokensListRepository: TokensListRepository): GetGlobalTokenListUseCase {
|
||||
return GetGlobalTokenListUseCase(repository = tokensListRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -67,15 +67,17 @@ internal class BiometricUserWalletsListManager(
|
|||
}
|
||||
}
|
||||
.map {
|
||||
if (throwIfNotAllWalletsUnlocked && state.value.userWallets.any(UserWallet::isLocked)) {
|
||||
Timber.e("Not all user wallets have been unlocked")
|
||||
val userWallets = state.value.userWallets
|
||||
|
||||
if (throwIfNotAllWalletsUnlocked && userWallets.any(UserWallet::isLocked)) {
|
||||
Timber.e("Some user wallets remain locked")
|
||||
throw UserWalletsListError.NotAllUserWalletsUnlocked
|
||||
}
|
||||
|
||||
val selectedUserWallet = selectedUserWalletSync
|
||||
if (selectedUserWallet == null || selectedUserWallet.isLocked) {
|
||||
Timber.e("Unable to find selected user wallet")
|
||||
throw UserWalletsListError.NoUserWalletSelected
|
||||
findAndSetUnlockedUserWallet(userWallets)
|
||||
?: throw UserWalletsListError.NoUserWalletSelected
|
||||
} else {
|
||||
selectedUserWallet
|
||||
}
|
||||
|
|
@ -280,13 +282,14 @@ internal class BiometricUserWalletsListManager(
|
|||
prevSelectedWalletId: UserWalletId?,
|
||||
userWallets: List<UserWallet>,
|
||||
): UserWalletId? {
|
||||
val findUnlockedAndSet = {
|
||||
userWallets.firstOrNull { !it.isLocked }
|
||||
?.walletId
|
||||
?.also { selectedUserWalletRepository.set(it) }
|
||||
}
|
||||
return prevSelectedWalletId
|
||||
?: (selectedUserWalletRepository.get() ?: findAndSetUnlockedUserWallet(userWallets)?.walletId)
|
||||
}
|
||||
|
||||
return prevSelectedWalletId ?: (selectedUserWalletRepository.get() ?: findUnlockedAndSet())
|
||||
private fun findAndSetUnlockedUserWallet(userWallets: List<UserWallet>): UserWallet? {
|
||||
return userWallets
|
||||
.firstOrNull { !it.isLocked }
|
||||
?.also { selectedUserWalletRepository.set(it.walletId) }
|
||||
}
|
||||
|
||||
private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List<UserWalletId>) {
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.components.SpacerW32
|
||||
import com.tangem.core.ui.components.TangemSwitch
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
|
||||
import com.tangem.core.ui.components.TangemSwitch
|
||||
|
||||
@Composable
|
||||
internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.core.navigation.AppScreen
|
|||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.NetworkAddress
|
||||
import com.tangem.feature.swap.presentation.SwapFragment
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
|
|
@ -111,7 +112,10 @@ class TradeCryptoMiddleware {
|
|||
}
|
||||
|
||||
private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) {
|
||||
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return
|
||||
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
|
||||
?.defaultAddress
|
||||
?.let(NetworkAddress.Address::value)
|
||||
?: return
|
||||
|
||||
val status = action.cryptoCurrencyStatus
|
||||
val currency = status.currency
|
||||
|
|
@ -194,7 +198,10 @@ class TradeCryptoMiddleware {
|
|||
}
|
||||
|
||||
private fun proceedNewSellAction(action: TradeCryptoAction.New.Sell) {
|
||||
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return
|
||||
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
|
||||
?.defaultAddress
|
||||
?.let(NetworkAddress.Address::value)
|
||||
?: return
|
||||
val currency = action.cryptoCurrencyStatus.currency
|
||||
|
||||
store.state.globalState.exchangeManager.getUrl(
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Core shouldn't depends on core, but in case with utils and logging its necessary */
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(projects.core.analytics.models)
|
||||
|
||||
implementation(deps.kotlin.coroutines)
|
||||
}
|
||||
|
|
@ -8,4 +8,7 @@ open class AnalyticsEvent(
|
|||
val event: String,
|
||||
var params: Map<String, String> = mapOf(),
|
||||
val error: Throwable? = null,
|
||||
)
|
||||
) {
|
||||
|
||||
val id: String = "[$category] $event"
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.analytics.models
|
||||
|
||||
interface OneTimeAnalyticsEvent {
|
||||
|
||||
val oneTimeEventId: String
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.tangem.core.analytics" />
|
||||
|
|
@ -16,7 +16,7 @@ interface AnalyticsEventFilter {
|
|||
* An internal filter check that, on external or internal conditions, recognizes the possibility of
|
||||
* sending an event.
|
||||
*/
|
||||
fun canBeSent(event: AnalyticsEvent): Boolean
|
||||
suspend fun canBeSent(event: AnalyticsEvent): Boolean
|
||||
|
||||
/**
|
||||
* Performs a check to see if the event can be dispatched by a specific handler
|
||||
|
|
|
|||
|
|
@ -12,13 +12,11 @@ interface AnalyticsEventHandler {
|
|||
interface AnalyticsHandler : AnalyticsEventHandler {
|
||||
fun id(): String
|
||||
|
||||
fun send(event: String, params: Map<String, String> = emptyMap())
|
||||
fun send(eventId: String, params: Map<String, String> = emptyMap())
|
||||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
send(prepareEventString(event), event.params)
|
||||
send(event.id, event.params)
|
||||
}
|
||||
|
||||
fun prepareEventString(event: AnalyticsEvent): String = "[${event.category}] ${event.event}"
|
||||
}
|
||||
|
||||
interface ErrorEventHandler {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.tangem.core.analytics.di
|
|||
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.filter.OneTimeEventFilter
|
||||
import com.tangem.core.analytics.repository.AnalyticsRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -10,11 +12,16 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class AnalyticsModule {
|
||||
internal object AnalyticsModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAnalyticsHandler(): AnalyticsEventHandler {
|
||||
return Analytics // todo replace after refactoring calling Analytics in whole project
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideOneTimeEventFilter(analyticsRepository: AnalyticsRepository): OneTimeEventFilter {
|
||||
return OneTimeEventFilter(analyticsRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.core.analytics.filter
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventFilter
|
||||
import com.tangem.core.analytics.api.AnalyticsHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.OneTimeAnalyticsEvent
|
||||
import com.tangem.core.analytics.repository.AnalyticsRepository
|
||||
|
||||
class OneTimeEventFilter(
|
||||
private val analyticsRepository: AnalyticsRepository,
|
||||
) : AnalyticsEventFilter {
|
||||
|
||||
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is OneTimeAnalyticsEvent
|
||||
|
||||
override suspend fun canBeSent(event: AnalyticsEvent): Boolean {
|
||||
if (event !is OneTimeAnalyticsEvent) return true
|
||||
|
||||
val isSent = analyticsRepository.checkIsEventSent(event.oneTimeEventId)
|
||||
|
||||
if (!isSent) {
|
||||
analyticsRepository.setIsEventSent(event.oneTimeEventId)
|
||||
}
|
||||
|
||||
return !isSent
|
||||
}
|
||||
|
||||
override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean {
|
||||
return canBeAppliedTo(event)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.core.analytics.repository
|
||||
|
||||
interface AnalyticsRepository {
|
||||
|
||||
suspend fun checkIsEventSent(eventId: String): Boolean
|
||||
|
||||
suspend fun setIsEventSent(eventId: String)
|
||||
}
|
||||
|
|
@ -37,6 +37,8 @@ object PreferencesKeys {
|
|||
|
||||
val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") }
|
||||
|
||||
val SENT_ONE_TIME_EVENTS_KEY by lazy { stringPreferencesKey(name = "sentOneTimeEvents") }
|
||||
|
||||
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="common_fee_error">Erhalt der Gebühr fehlgeschlagen</string>
|
||||
<string name="no_account_generic">Laden Sie %1$s+ %2$s auf um ein Konto zu erstellen</string>
|
||||
<string name="send_error_dust_amount_format">Minimaler Betrag ist %s</string>
|
||||
<string name="send_error_dust_change">Restbestand zu klein</string>
|
||||
<string name="send_error_invalid_fee_value">Falsche Gebühr</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="common_fee_error">Échec de réception des commissions</string>
|
||||
<string name="no_account_generic">Pour créer un compte, téléchargez %1$s+ %2$s</string>
|
||||
<string name="send_error_dust_amount_format">Le montant minimal est de %s</string>
|
||||
<string name="send_error_dust_change">Le reste est trop petit</string>
|
||||
<string name="send_error_invalid_fee_value">Commission non valide</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="common_fee_error">Impossibile ottenere la commissione</string>
|
||||
<string name="no_account_generic">Scarica %1$s+ %2$s per creare un account</string>
|
||||
<string name="send_error_dust_amount_format">L\'importo minimo è di %s</string>
|
||||
<string name="send_error_dust_change">L\'importo residuo è molto basso</string>
|
||||
<string name="send_error_invalid_fee_value">Commissione non valida</string>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
<string name="address_type_legacy">遺留資產</string>
|
||||
<string name="common_fee_error">獲取費用失敗</string>
|
||||
<string name="kaspa_withdrawal_message_warning">由於 Kaspa 的限制,只有%1$d UTXO 可以放入單次交易中。這意味著您只能發送%2$s或更少數量。您需要減少數量。</string>
|
||||
<string name="no_account_generic">加載 %1$s+ %2$s 以創建帳戶</string>
|
||||
<string name="no_account_polkadot">目標帳戶未激活。發送 %s 或更多以激活帳戶</string>
|
||||
<string name="send_error_dust_amount_format">最小數量是 %s</string>
|
||||
<string name="send_error_dust_change">更動太小</string>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import kotlinx.coroutines.launch
|
|||
@Composable
|
||||
inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
||||
config: TangemBottomSheetConfig,
|
||||
color: Color = TangemTheme.colors.background.primary,
|
||||
contentColor: Color = TangemTheme.colors.background.primary,
|
||||
crossinline content: @Composable ColumnScope.(T) -> Unit,
|
||||
) {
|
||||
var isVisible by remember { mutableStateOf(value = config.isShow) }
|
||||
|
|
@ -31,7 +31,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
|||
ModalBottomSheet(
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = color,
|
||||
containerColor = contentColor,
|
||||
shape = TangemTheme.shapes.bottomSheetLarge,
|
||||
dragHandle = { TangemBottomSheetDraggableHeader(color) },
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.core.ui.R
|
|||
import com.tangem.core.ui.components.SimpleSettingsRow
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
|
|
@ -24,7 +25,7 @@ private fun ChooseAddressBottomSheetContent(content: ChooseAddressBottomSheetCon
|
|||
) {
|
||||
content.addressModels.forEach { addressModel ->
|
||||
SimpleSettingsRow(
|
||||
title = addressModel.type.name,
|
||||
title = addressModel.displayName.resolveReference(),
|
||||
icon = R.drawable.ic_arrow_top_right_24,
|
||||
onItemsClick = { content.onClick(addressModel) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.core.ui.components.bottomsheets.tokenreceive
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.NetworkAddress
|
||||
|
||||
private const val DSC_ADDRESS_NAME = "DSC"
|
||||
private const val DEL_ADDRESS_NAME = "Main"
|
||||
|
||||
private const val LEGACY_ADDRESS_NAME = "Legacy"
|
||||
private const val DEFAULT_ADDRESS_NAME = "Default"
|
||||
|
||||
fun Set<NetworkAddress.Address>.mapToAddressModels(currency: CryptoCurrency): List<AddressModel> = this
|
||||
.sortedBy { it.type.ordinal }
|
||||
.map { address ->
|
||||
AddressModel(
|
||||
displayName = currency.network.getAddressDisplayName(address.type),
|
||||
value = address.value,
|
||||
type = when (address.type) {
|
||||
NetworkAddress.Address.Type.Primary -> AddressModel.Type.Default
|
||||
NetworkAddress.Address.Type.Secondary -> AddressModel.Type.Legacy
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun Network.getAddressDisplayName(addressType: NetworkAddress.Address.Type): TextReference {
|
||||
return when (id.value) {
|
||||
"decimal", "decimal/test" -> {
|
||||
when (addressType) {
|
||||
NetworkAddress.Address.Type.Primary -> stringReference(value = DEL_ADDRESS_NAME)
|
||||
NetworkAddress.Address.Type.Secondary -> stringReference(value = DSC_ADDRESS_NAME)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
when (addressType) {
|
||||
NetworkAddress.Address.Type.Primary -> stringReference(value = DEFAULT_ADDRESS_NAME)
|
||||
NetworkAddress.Address.Type.Secondary -> stringReference(value = LEGACY_ADDRESS_NAME)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
package com.tangem.core.ui.components.bottomsheets.tokenreceive
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
data class AddressModel(
|
||||
val displayName: TextReference,
|
||||
val value: String,
|
||||
val type: Type = Type.Default,
|
||||
val type: Type,
|
||||
) {
|
||||
enum class Type {
|
||||
Legacy, Default
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -26,6 +26,7 @@ import com.tangem.core.ui.components.SecondaryButtonIconStart
|
|||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.rememberQrPainters
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.shareText
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
|
|
@ -181,13 +182,6 @@ private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String
|
|||
return if (content.addresses.size < 2) {
|
||||
content.name
|
||||
} else {
|
||||
"${
|
||||
stringResource(
|
||||
id = when (content.addresses[index].type) {
|
||||
AddressModel.Type.Default -> R.string.address_type_default
|
||||
AddressModel.Type.Legacy -> R.string.address_type_legacy
|
||||
},
|
||||
)
|
||||
} ${content.name}"
|
||||
"${content.addresses[index].displayName.resolveReference()} ${content.name}"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
package com.tangem.core.ui.components.bottomsheets.tokenreceive
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
class TokenReceiveBottomSheetConfig(
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val network: String,
|
||||
val addresses: List<AddressModel>,
|
||||
val addresses: ImmutableList<AddressModel>,
|
||||
val onCopyClick: () -> Unit,
|
||||
val onShareClick: () -> Unit,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -28,7 +28,6 @@ fun getActiveIconRes(blockchainId: String): Int {
|
|||
"GNO" -> R.drawable.img_gnosis_22
|
||||
"ETH-Pow", "ETH-Pow/test" -> R.drawable.img_eth_pow_22
|
||||
"ETH-Fair" -> R.drawable.img_eth_fair_22
|
||||
"NEAR", "NEAR/test" -> R.drawable.img_near_22
|
||||
"Polkadot", "Polkadot/test" -> R.drawable.img_polkadot_22
|
||||
"Kusama" -> R.drawable.img_kusama_22
|
||||
"OPTIMISM", "OPTIMISM/test" -> R.drawable.img_optimism_22
|
||||
|
|
|
|||
|
|
@ -32,6 +32,16 @@ fun CryptoCurrency.Token.tryGetBackgroundForTokenIcon(
|
|||
): Color {
|
||||
if (isGrayscale) return TangemColorPalette.Dark2
|
||||
|
||||
return tryGetBackgroundForTokenIcon(contractAddress = contractAddress, fallbackColor = fallbackColor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to extract a background color from the contract address of a token.
|
||||
*
|
||||
* @param fallbackColor The color to use as a fallback.
|
||||
* @return The extracted background color or the fallback color if extraction fails or if it is a test network token.
|
||||
*/
|
||||
fun tryGetBackgroundForTokenIcon(contractAddress: String, fallbackColor: Color = TangemColorPalette.Black): Color {
|
||||
return try {
|
||||
val colorHex = "#" + contractAddress.substring(range = COLOR_HEX_START_INDEX..COLOR_HEX_END_INDEX)
|
||||
Color(colorHex.toColorInt())
|
||||
|
|
|
|||
1
data/analytics/.gitignore
vendored
Normal file
1
data/analytics/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/buld
|
||||
32
data/analytics/build.gradle.kts
Normal file
32
data/analytics/build.gradle.kts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.data.analytics"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Project - Analytics */
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
|
||||
/** Project - Data */
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.data.common)
|
||||
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.datastore)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.timber)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.data.analytics
|
||||
|
||||
import com.tangem.core.analytics.repository.AnalyticsRepository
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectListSync
|
||||
|
||||
internal class DefaultAnalyticsRepository(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : AnalyticsRepository {
|
||||
|
||||
override suspend fun checkIsEventSent(eventId: String): Boolean {
|
||||
val sentEvents = appPreferencesStore
|
||||
.getObjectListSync<String>(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY)
|
||||
|
||||
return eventId in sentEvents
|
||||
}
|
||||
|
||||
override suspend fun setIsEventSent(eventId: String) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val sentEvents = mutablePreferences.getObject<List<String>>(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY)
|
||||
val updatedSentEvents = sentEvents.orEmpty() + eventId
|
||||
|
||||
mutablePreferences.setObject(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY, updatedSentEvents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.data.analytics.di
|
||||
|
||||
import com.tangem.core.analytics.repository.AnalyticsRepository
|
||||
import com.tangem.data.analytics.DefaultAnalyticsRepository
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object AnalyticsDataModule {
|
||||
|
||||
@Provides
|
||||
fun provideAnalyticsRepository(appPreferencesStore: AppPreferencesStore): AnalyticsRepository {
|
||||
return DefaultAnalyticsRepository(appPreferencesStore)
|
||||
}
|
||||
}
|
||||
|
|
@ -45,4 +45,5 @@ dependencies {
|
|||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.retrofit) // For HttpException
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
}
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
package com.tangem.data.tokens.di
|
||||
|
||||
import com.tangem.data.common.cache.CacheRegistry
|
||||
import com.tangem.data.tokens.repository.DefaultCurrenciesRepository
|
||||
import com.tangem.data.tokens.repository.DefaultMarketCryptoCurrencyRepository
|
||||
import com.tangem.data.tokens.repository.DefaultNetworksRepository
|
||||
import com.tangem.data.tokens.repository.DefaultQuotesRepository
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.data.tokens.repository.*
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.network.NetworksStatusesStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
@ -13,10 +9,7 @@ import com.tangem.datasource.local.quote.QuotesStore
|
|||
import com.tangem.datasource.local.token.AssetsStore
|
||||
import com.tangem.datasource.local.token.UserTokensStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.tokens.repository.*
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -94,4 +87,18 @@ internal object TokensDataModule {
|
|||
fun provideDefaultMarketCoinsRepository(assetsStore: AssetsStore): MarketCryptoCurrencyRepository {
|
||||
return DefaultMarketCryptoCurrencyRepository(assetsStore)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesTokensListRepository(
|
||||
tangemTechApi: TangemTechApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
quotesRepository: QuotesRepository,
|
||||
): TokensListRepository {
|
||||
return DefaultTokensListRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
quotesRepository = quotesRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.tangem.data.tokens.paging
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Quote
|
||||
import com.tangem.domain.tokens.model.Token
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
||||
/**
|
||||
* A PagingSource responsible for retrieving tokens from the Tangem Tech API and adding to them quotes information.
|
||||
*
|
||||
* @property api Tangem Tech API
|
||||
* @property quotesRepository Repository providing quotes data.
|
||||
* @property dispatchers Coroutine dispatchers provider.
|
||||
* @property searchText The search text used to filter tokens.
|
||||
*/
|
||||
internal class CoinsPagingSource(
|
||||
private val api: TangemTechApi,
|
||||
private val quotesRepository: QuotesRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val searchText: String?,
|
||||
) : PagingSource<Int, Token>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, Token>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1)
|
||||
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(other = 1)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Token> {
|
||||
val page = params.key ?: 0
|
||||
|
||||
return com.tangem.utils.coroutines.runCatching(dispatchers.io) {
|
||||
api.getCoins(
|
||||
active = true, // TODO change when voting functionality is implemented
|
||||
searchText = searchText,
|
||||
offset = page * params.loadSize,
|
||||
limit = params.loadSize,
|
||||
)
|
||||
}.fold(
|
||||
onSuccess = { response ->
|
||||
val coinsIds = response.coins.map { coin ->
|
||||
CryptoCurrency.ID(
|
||||
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
|
||||
body = CryptoCurrency.ID.Body.NetworkId(coin.id),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID(coin.id),
|
||||
)
|
||||
}
|
||||
val quotes = quotesRepository.getQuotesSync(currenciesIds = coinsIds.toSet(), refresh = false)
|
||||
|
||||
LoadResult.Page(
|
||||
data = CoinsResponseConverter.convert(
|
||||
CoinsData(
|
||||
response.coins,
|
||||
response.imageHost,
|
||||
quotes,
|
||||
),
|
||||
),
|
||||
prevKey = if (page == 0) null else page.minus(other = 1),
|
||||
nextKey = if (response.coins.isEmpty()) null else page.plus(other = 1),
|
||||
)
|
||||
},
|
||||
onFailure = { LoadResult.Error(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class CoinsData(
|
||||
val coins: List<CoinsResponse.Coin>,
|
||||
val imageHost: String?,
|
||||
val quotes: Set<Quote>,
|
||||
)
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.data.tokens.paging
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.data.tokens.utils.getNetworkStandardType
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Token
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from data model [CoinsResponse] to list of domain models [Token]
|
||||
*/
|
||||
internal object CoinsResponseConverter : Converter<CoinsData, List<Token>> {
|
||||
|
||||
override fun convert(value: CoinsData): List<Token> {
|
||||
return value.coins.map { coin ->
|
||||
val id = CryptoCurrency.ID(
|
||||
CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
|
||||
CryptoCurrency.ID.Body.NetworkId(coin.id),
|
||||
CryptoCurrency.ID.Suffix.RawID(coin.id),
|
||||
)
|
||||
val quote = value.quotes.find { it.rawCurrencyId == id.rawCurrencyId }
|
||||
Token(
|
||||
id = id.rawCurrencyId ?: coin.name,
|
||||
name = coin.name,
|
||||
symbol = coin.symbol,
|
||||
iconUrl = getIconUrl(coin.id, value.imageHost),
|
||||
isAvailable = coin.active,
|
||||
networks = coin.networks.mapNotNull { network ->
|
||||
val blockchain = Blockchain.fromNetworkId(network.networkId) ?: return@mapNotNull null
|
||||
Token.Network(
|
||||
networkId = network.networkId,
|
||||
standardType = getNetworkStandardType(blockchain).name,
|
||||
address = network.contractAddress,
|
||||
iconUrl = getIconUrl(network.networkId, value.imageHost),
|
||||
decimalCount = network.decimalCount?.toInt(),
|
||||
)
|
||||
},
|
||||
quote = quote,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getIconUrl(id: String, imageHost: String? = null): String {
|
||||
return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png"
|
||||
}
|
||||
|
||||
private const val DEFAULT_IMAGE_HOST =
|
||||
"https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.data.tokens.paging.CoinsPagingSource
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.domain.tokens.model.Token
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.tokens.repository.TokensListRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Default repository implementation for managing operations related to a complete set of tokens
|
||||
*
|
||||
* @property tangemTechApi Tangem Tech API
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
* @property quotesRepository responsible for providing cryptocurrency quotes data
|
||||
*
|
||||
*/
|
||||
internal class DefaultTokensListRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val quotesRepository: QuotesRepository,
|
||||
) : TokensListRepository {
|
||||
|
||||
override fun getTokens(searchText: String?): Flow<PagingData<Token>> {
|
||||
return Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = 100,
|
||||
prefetchDistance = 70,
|
||||
enablePlaceholders = false,
|
||||
),
|
||||
pagingSourceFactory = {
|
||||
CoinsPagingSource(
|
||||
api = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
searchText = searchText,
|
||||
quotesRepository = quotesRepository,
|
||||
)
|
||||
},
|
||||
).flow
|
||||
}
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ private fun getNetworkDerivationPath(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
|
||||
internal fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType {
|
||||
return when (blockchain) {
|
||||
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20
|
||||
Blockchain.BSC, Blockchain.BSCTestnet -> Network.StandardType.BEP20
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.data.tokens.utils
|
|||
|
||||
import com.tangem.domain.tokens.model.*
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.walletmanager.model.Address
|
||||
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
|
||||
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
|
||||
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
|
||||
|
|
@ -20,12 +21,12 @@ internal class NetworkStatusFactory {
|
|||
is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation
|
||||
is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable
|
||||
is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount(
|
||||
address = getNetworkAddress(result.defaultAddress, result.addresses),
|
||||
address = getNetworkAddress(result.selectedAddress, result.addresses),
|
||||
amountToCreateAccount = result.amountToCreateAccount,
|
||||
errorMessage = result.errorMessage,
|
||||
)
|
||||
is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified(
|
||||
address = getNetworkAddress(result.defaultAddress, result.addresses),
|
||||
address = getNetworkAddress(result.selectedAddress, result.addresses),
|
||||
amounts = formatAmounts(result.currenciesAmounts, currencies),
|
||||
pendingTransactions = formatTransactions(
|
||||
transactions = result.currentTransactions,
|
||||
|
|
@ -92,11 +93,26 @@ internal class NetworkStatusFactory {
|
|||
return transactions.mapTo(hashSetOf()) { it.txHistoryItem }
|
||||
}
|
||||
|
||||
private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set<String>): NetworkAddress {
|
||||
private fun getNetworkAddress(selectedAddress: String, availableAddresses: Set<Address>): NetworkAddress {
|
||||
val defaultAddress = availableAddresses
|
||||
.firstOrNull { it.value == selectedAddress }
|
||||
?.let(::mapToDomainAddress)
|
||||
|
||||
requireNotNull(defaultAddress) { "Selected address must not be null" }
|
||||
|
||||
return if (availableAddresses.size != 1) {
|
||||
NetworkAddress.Selectable(defaultAddress, availableAddresses)
|
||||
NetworkAddress.Selectable(defaultAddress, availableAddresses.mapTo(hashSetOf(), ::mapToDomainAddress))
|
||||
} else {
|
||||
NetworkAddress.Single(defaultAddress)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapToDomainAddress(address: Address): NetworkAddress.Address {
|
||||
val type = when (address.type) {
|
||||
Address.Type.Primary -> NetworkAddress.Address.Type.Primary
|
||||
Address.Type.Secondary -> NetworkAddress.Address.Type.Secondary
|
||||
}
|
||||
|
||||
return NetworkAddress.Address(address.value, type)
|
||||
}
|
||||
}
|
||||
|
|
@ -195,8 +195,7 @@ fun Blockchain.toCoinId(): String {
|
|||
Blockchain.Chia, Blockchain.ChiaTestnet -> "chia"
|
||||
Blockchain.Near -> "near"
|
||||
Blockchain.NearTestnet -> "near/test"
|
||||
Blockchain.Decimal -> "decimal"
|
||||
Blockchain.DecimalTestnet -> "decimal/test"
|
||||
Blockchain.Decimal, Blockchain.DecimalTestnet -> "decimal"
|
||||
Blockchain.Unknown -> "unknown"
|
||||
}
|
||||
}
|
||||
|
|
@ -224,6 +223,4 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5
|
|||
private val excludedBlockchains = listOf(
|
||||
Blockchain.Unknown,
|
||||
Blockchain.Ducatus,
|
||||
Blockchain.Decimal,
|
||||
Blockchain.DecimalTestnet,
|
||||
)
|
||||
|
|
@ -116,6 +116,7 @@ interface WalletManagersFacade {
|
|||
* @param userWalletId selected wallet id
|
||||
* @param network network of currency
|
||||
*/
|
||||
@Deprecated("Use NetworkAddress from CryptoCurrencyStatus")
|
||||
suspend fun getAddress(userWalletId: UserWalletId, network: Network): List<Address>
|
||||
|
||||
/** Returns list of all addresses for all currencies in selected wallet
|
||||
|
|
@ -123,6 +124,7 @@ interface WalletManagersFacade {
|
|||
* @param userWalletId selected wallet id
|
||||
* @param network required to create wallet manager
|
||||
*/
|
||||
@Deprecated("Use NetworkAddress from CryptoCurrencyStatus")
|
||||
suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set<Address>
|
||||
|
||||
/**
|
||||
|
|
@ -156,6 +158,7 @@ interface WalletManagersFacade {
|
|||
* @param userWalletId selected wallet id
|
||||
* @param network network of currency
|
||||
*/
|
||||
@Deprecated("Will be removed in future")
|
||||
suspend fun getFee(
|
||||
amount: Amount,
|
||||
destination: String,
|
||||
|
|
@ -171,6 +174,7 @@ interface WalletManagersFacade {
|
|||
* @param userWalletId selected wallet id
|
||||
* @param network network of currency
|
||||
*/
|
||||
@Deprecated("Will be removed in future")
|
||||
suspend fun validateTransaction(
|
||||
amount: Amount,
|
||||
fee: Amount?,
|
||||
|
|
@ -189,6 +193,7 @@ interface WalletManagersFacade {
|
|||
* @param network network of currency
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Deprecated("Will be removed in future")
|
||||
suspend fun createTransaction(
|
||||
amount: Amount,
|
||||
fee: Fee,
|
||||
|
|
@ -206,6 +211,7 @@ interface WalletManagersFacade {
|
|||
* @param userWalletId selected wallet id
|
||||
* @param network network of currency
|
||||
*/
|
||||
@Deprecated("Will be removed in future")
|
||||
suspend fun sendTransaction(
|
||||
txData: TransactionData,
|
||||
signer: CommonSigner,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.domain.walletmanager.model
|
||||
|
||||
data class Address(
|
||||
val value: String,
|
||||
val type: Type,
|
||||
) {
|
||||
|
||||
enum class Type {
|
||||
Primary, Secondary,
|
||||
}
|
||||
}
|
||||
|
|
@ -9,15 +9,15 @@ sealed class UpdateWalletManagerResult {
|
|||
object Unreachable : UpdateWalletManagerResult()
|
||||
|
||||
data class Verified(
|
||||
val defaultAddress: String,
|
||||
val addresses: Set<String>,
|
||||
val selectedAddress: String,
|
||||
val addresses: Set<Address>,
|
||||
val currenciesAmounts: Set<CryptoCurrencyAmount>,
|
||||
val currentTransactions: Set<CryptoCurrencyTransaction>,
|
||||
) : UpdateWalletManagerResult()
|
||||
|
||||
data class NoAccount(
|
||||
val defaultAddress: String,
|
||||
val addresses: Set<String>,
|
||||
val selectedAddress: String,
|
||||
val addresses: Set<Address>,
|
||||
val amountToCreateAccount: BigDecimal,
|
||||
val errorMessage: String,
|
||||
) : UpdateWalletManagerResult()
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
package com.tangem.domain.walletmanager.utils
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.domain.common.extensions.amountToCreateAccount
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.walletmanager.model.Address
|
||||
import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount
|
||||
import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction
|
||||
import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.util.concurrent.TimeUnit
|
||||
import com.tangem.blockchain.common.address.Address as SdkAddress
|
||||
|
||||
internal class UpdateWalletManagerResultFactory {
|
||||
|
||||
|
|
@ -18,7 +20,7 @@ internal class UpdateWalletManagerResultFactory {
|
|||
val addresses = getAvailableAddresses(wallet.addresses)
|
||||
|
||||
return UpdateWalletManagerResult.Verified(
|
||||
defaultAddress = wallet.address,
|
||||
selectedAddress = wallet.address,
|
||||
addresses = addresses,
|
||||
currenciesAmounts = getTokensAmounts(wallet.amounts.values.toSet()),
|
||||
currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()),
|
||||
|
|
@ -30,7 +32,7 @@ internal class UpdateWalletManagerResultFactory {
|
|||
val addresses = getAvailableAddresses(wallet.addresses)
|
||||
|
||||
return UpdateWalletManagerResult.Verified(
|
||||
defaultAddress = wallet.address,
|
||||
selectedAddress = wallet.address,
|
||||
addresses = addresses,
|
||||
currenciesAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens),
|
||||
currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()),
|
||||
|
|
@ -48,7 +50,7 @@ internal class UpdateWalletManagerResultFactory {
|
|||
UpdateWalletManagerResult.Unreachable
|
||||
} else {
|
||||
UpdateWalletManagerResult.NoAccount(
|
||||
defaultAddress = wallet.address,
|
||||
selectedAddress = wallet.address,
|
||||
addresses = getAvailableAddresses(wallet.addresses),
|
||||
amountToCreateAccount = amountToCreateAccount,
|
||||
errorMessage = customMessage,
|
||||
|
|
@ -70,7 +72,7 @@ internal class UpdateWalletManagerResultFactory {
|
|||
}
|
||||
|
||||
private fun getCurrentTransactions(
|
||||
walletAddresses: Set<String>,
|
||||
walletAddresses: Set<Address>,
|
||||
recentTransactions: Set<TransactionData>,
|
||||
): Set<CryptoCurrencyTransaction> {
|
||||
val unconfirmedTransactions = recentTransactions.filter {
|
||||
|
|
@ -95,7 +97,7 @@ internal class UpdateWalletManagerResultFactory {
|
|||
}
|
||||
|
||||
private fun createCurrencyTransaction(
|
||||
walletAddresses: Set<String>,
|
||||
walletAddresses: Set<Address>,
|
||||
data: TransactionData,
|
||||
): CryptoCurrencyTransaction? {
|
||||
return when (val type = data.amount.type) {
|
||||
|
|
@ -115,11 +117,11 @@ internal class UpdateWalletManagerResultFactory {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createTxHistoryItem(walletAddresses: Set<String>, data: TransactionData): TxHistoryItem? {
|
||||
private fun createTxHistoryItem(walletAddresses: Set<Address>, data: TransactionData): TxHistoryItem? {
|
||||
val hash = data.hash ?: return null
|
||||
val millis = data.date?.timeInMillis ?: return null
|
||||
val amount = getTransactionAmountValue(data.amount) ?: return null
|
||||
val isOutgoing = data.sourceAddress in walletAddresses
|
||||
val isOutgoing = data.sourceAddress in walletAddresses.map { it.value }
|
||||
|
||||
return TxHistoryItem(
|
||||
txHash = hash,
|
||||
|
|
@ -141,8 +143,16 @@ internal class UpdateWalletManagerResultFactory {
|
|||
)
|
||||
}
|
||||
|
||||
private fun getAvailableAddresses(addresses: Set<Address>): Set<String> {
|
||||
return addresses.mapTo(hashSetOf()) { it.value }
|
||||
private fun getAvailableAddresses(addresses: Set<SdkAddress>): Set<Address> {
|
||||
return addresses.mapTo(hashSetOf()) { sdkAddress ->
|
||||
Address(
|
||||
value = sdkAddress.value,
|
||||
type = when (sdkAddress.type) {
|
||||
AddressType.Default -> Address.Type.Primary
|
||||
AddressType.Legacy -> Address.Type.Secondary
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrencyAmountValue(amount: Amount): BigDecimal? {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ dependencies {
|
|||
/** Project - Other */
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Android - Other */
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
|
||||
/** Utils */
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.reKotlin)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ data class CryptoCurrencyStatus(
|
|||
override val fiatAmount: BigDecimal?,
|
||||
override val priceChange: BigDecimal?,
|
||||
override val fiatRate: BigDecimal?,
|
||||
override val networkAddress: NetworkAddress?,
|
||||
override val networkAddress: NetworkAddress,
|
||||
) : Status(isError = false) {
|
||||
|
||||
override val amount: BigDecimal = BigDecimal.ZERO
|
||||
|
|
@ -102,7 +102,7 @@ data class CryptoCurrencyStatus(
|
|||
override val priceChange: BigDecimal,
|
||||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<TxHistoryItem>,
|
||||
override val networkAddress: NetworkAddress?,
|
||||
override val networkAddress: NetworkAddress,
|
||||
) : Status(isError = false)
|
||||
|
||||
/**
|
||||
|
|
@ -123,7 +123,7 @@ data class CryptoCurrencyStatus(
|
|||
override val priceChange: BigDecimal?,
|
||||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<TxHistoryItem>,
|
||||
override val networkAddress: NetworkAddress?,
|
||||
override val networkAddress: NetworkAddress,
|
||||
) : Status(isError = false)
|
||||
|
||||
/**
|
||||
|
|
@ -138,6 +138,6 @@ data class CryptoCurrencyStatus(
|
|||
override val amount: BigDecimal,
|
||||
override val hasCurrentNetworkTransactions: Boolean,
|
||||
override val pendingTransactions: Set<TxHistoryItem>,
|
||||
override val networkAddress: NetworkAddress?,
|
||||
override val networkAddress: NetworkAddress,
|
||||
) : Status(isError = false)
|
||||
}
|
||||
|
|
@ -6,18 +6,19 @@ package com.tangem.domain.tokens.model
|
|||
sealed class NetworkAddress {
|
||||
|
||||
/** The default or currently selected network address. */
|
||||
abstract val defaultAddress: String
|
||||
abstract val defaultAddress: Address
|
||||
|
||||
/** The set of available network addresses to choose from. */
|
||||
abstract val availableAddresses: Set<Address>
|
||||
|
||||
/**
|
||||
* Represents a single static network address.
|
||||
*
|
||||
* @property defaultAddress The static network address.
|
||||
*/
|
||||
data class Single(override val defaultAddress: String) : NetworkAddress() {
|
||||
data class Single(override val defaultAddress: Address) : NetworkAddress() {
|
||||
|
||||
init {
|
||||
checkDefaultAddress()
|
||||
}
|
||||
override val availableAddresses: Set<Address> = setOf(defaultAddress)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -27,17 +28,26 @@ sealed class NetworkAddress {
|
|||
* @property availableAddresses The set of available network addresses to choose from.
|
||||
*/
|
||||
data class Selectable(
|
||||
override val defaultAddress: String,
|
||||
val availableAddresses: Set<String>,
|
||||
override val defaultAddress: Address,
|
||||
override val availableAddresses: Set<Address>,
|
||||
) : NetworkAddress() {
|
||||
|
||||
init {
|
||||
checkDefaultAddress()
|
||||
require(availableAddresses.isNotEmpty()) { "Available network addresses must not be empty" }
|
||||
}
|
||||
}
|
||||
|
||||
protected fun checkDefaultAddress() {
|
||||
require(defaultAddress.isNotBlank()) { "Selected network address must not be blank" }
|
||||
data class Address(
|
||||
val value: String,
|
||||
val type: Type,
|
||||
) {
|
||||
|
||||
enum class Type {
|
||||
Primary, Secondary,
|
||||
}
|
||||
|
||||
init {
|
||||
require(value.isNotBlank()) { "Address value must not be blank" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,14 +3,17 @@ package com.tangem.domain.tokens.model
|
|||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Represents a financial quote for a specific cryptocurrency, including its fiat exchange rate and price change.
|
||||
* Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change.
|
||||
*
|
||||
* @property rawCurrencyId The unique identifier of the token for which the quote is provided.
|
||||
* @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided.
|
||||
* @property fiatRate The current fiat exchange rate for the cryptocurrency.
|
||||
* @property priceChange The price change for the cryptocurrency.
|
||||
* @property values The values representing the cryptocurrency's price changes over a 24-hour period,
|
||||
* suitable for chart plotting.
|
||||
*/
|
||||
data class Quote(
|
||||
val rawCurrencyId: String,
|
||||
val fiatRate: BigDecimal,
|
||||
val priceChange: BigDecimal,
|
||||
val values: List<Double>? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.domain.tokens.model
|
||||
|
||||
/**
|
||||
* Represents a domain model for a token (as part of a general list of tokens).
|
||||
*
|
||||
* @property id The unique identifier of the token.
|
||||
* @property networks List of networks associated with the token.
|
||||
* @property isAvailable Indicates whether this token is supported in Tangem app.
|
||||
* @property quote Equivalent prices for the token.
|
||||
* @property name The name of the token.
|
||||
* @property symbol The brief name of the token, e.g., "BTC".
|
||||
* @property iconUrl URL of the token's icon.
|
||||
*/
|
||||
data class Token(
|
||||
val id: String,
|
||||
val networks: List<Network>,
|
||||
val isAvailable: Boolean,
|
||||
val quote: Quote?,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val iconUrl: String,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Represents a domain model for a network associated with a token.
|
||||
*
|
||||
* @property networkId The unique identifier of the network.
|
||||
* @property standardType The type of blockchain associated with the network.
|
||||
* @property address The contract address of the token on the current network.
|
||||
* It is null for the main currencies of the network.
|
||||
* @property iconUrl URL of the network's icon.
|
||||
* @property decimalCount The decimal count associated with the token on the network.
|
||||
*/
|
||||
data class Network(
|
||||
val networkId: String,
|
||||
val standardType: String,
|
||||
val address: String?,
|
||||
val iconUrl: String,
|
||||
val decimalCount: Int?,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.domain.tokens.model.Token
|
||||
import com.tangem.domain.tokens.repository.TokensListRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Use case for retrieving a complete set of tokens with their quotes (filtered by a search text if provided).
|
||||
*
|
||||
* @property repository The repository responsible for fetching and preparing the list of tokens with their quotes.
|
||||
*/
|
||||
class GetGlobalTokenListUseCase(private val repository: TokensListRepository) {
|
||||
|
||||
/**
|
||||
* @param searchText The text used for filtering the list of tokens (not required).
|
||||
* @return A [Flow] emitting [PagingData] containing the tokens with their quotes.
|
||||
*/
|
||||
operator fun invoke(searchText: String?): Flow<PagingData<Token>> {
|
||||
return repository.getTokens(searchText = searchText?.ifBlank(defaultValue = { null }))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.domain.tokens.repository
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.domain.tokens.model.Token
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Repository interface for managing operations related to a complete set of tokens
|
||||
* (not associated with any specific card).
|
||||
* */
|
||||
interface TokensListRepository {
|
||||
|
||||
/**
|
||||
* Retrieves a list of available tokens with quotes, can be filtered based on the provided search text.
|
||||
*
|
||||
* @param searchText The search text used to filter tokens.
|
||||
* @return A [Flow] emitting [PagingData] containing the tokens with quotes matching the search criteria.
|
||||
*/
|
||||
fun getTokens(searchText: String?): Flow<PagingData<Token>>
|
||||
}
|
||||
|
|
@ -118,7 +118,9 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest {
|
|||
amount = BigDecimal.TEN,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
pendingTransactions = emptySet(),
|
||||
networkAddress = NetworkAddress.Single(defaultAddress = "mock"),
|
||||
networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,9 @@ internal object MockNetworks {
|
|||
network = network3,
|
||||
value = NetworkStatus.NoAccount(
|
||||
amountToCreateAccount = amountToCreateAccount,
|
||||
address = NetworkAddress.Single(defaultAddress = "mock"),
|
||||
address = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
|
||||
),
|
||||
errorMessage = "",
|
||||
),
|
||||
)
|
||||
|
|
@ -73,7 +75,9 @@ internal object MockNetworks {
|
|||
MockTokens.token3.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
|
||||
),
|
||||
pendingTransactions = emptyMap(),
|
||||
address = NetworkAddress.Single(defaultAddress = "mock"),
|
||||
address = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -86,7 +90,9 @@ internal object MockNetworks {
|
|||
MockTokens.token6.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
|
||||
),
|
||||
pendingTransactions = emptyMap(),
|
||||
address = NetworkAddress.Single(defaultAddress = "mock"),
|
||||
address = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -100,7 +106,9 @@ internal object MockNetworks {
|
|||
MockTokens.token10.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN),
|
||||
),
|
||||
pendingTransactions = emptyMap(),
|
||||
address = NetworkAddress.Single(defaultAddress = "mock"),
|
||||
address = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.domain.tokens.mock
|
|||
import arrow.core.nonEmptyListOf
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.NetworkAddress
|
||||
import com.tangem.domain.tokens.model.NetworkStatus
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -64,7 +65,9 @@ internal object MockTokensStates {
|
|||
priceChange = MockQuotes.quote7.priceChange,
|
||||
fiatRate = MockQuotes.quote7.fiatRate,
|
||||
amountToCreateAccount = MockNetworks.amountToCreateAccount,
|
||||
networkAddress = null,
|
||||
networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -75,7 +78,9 @@ internal object MockTokensStates {
|
|||
priceChange = MockQuotes.quote8.priceChange,
|
||||
fiatRate = MockQuotes.quote8.fiatRate,
|
||||
amountToCreateAccount = MockNetworks.amountToCreateAccount,
|
||||
networkAddress = null,
|
||||
networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -86,7 +91,9 @@ internal object MockTokensStates {
|
|||
priceChange = MockQuotes.quote9.priceChange,
|
||||
fiatRate = MockQuotes.quote9.fiatRate,
|
||||
amountToCreateAccount = MockNetworks.amountToCreateAccount,
|
||||
networkAddress = null,
|
||||
networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -97,7 +104,9 @@ internal object MockTokensStates {
|
|||
priceChange = MockQuotes.quote10.priceChange,
|
||||
fiatRate = MockQuotes.quote10.fiatRate,
|
||||
amountToCreateAccount = MockNetworks.amountToCreateAccount,
|
||||
networkAddress = null,
|
||||
networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ dependencies {
|
|||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Project - Data */
|
||||
implementation(projects.data.tokens)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.common)
|
||||
implementation(projects.domain.card)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.managetokens.presentation.common.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
|
||||
@Composable
|
||||
internal fun ChooseWalletBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet<ChooseWalletBottomSheetConfig>(
|
||||
config = config,
|
||||
contentColor = TangemTheme.colors.background.tertiary,
|
||||
) {
|
||||
ChooseWalletScreen(state = it.chooseWalletState)
|
||||
}
|
||||
}
|
||||
|
||||
internal class ChooseWalletBottomSheetConfig(
|
||||
val chooseWalletState: ChooseWalletState.Choose,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.common.state.Event
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal data class ManageTokensState(
|
||||
val searchBarState: SearchBarState,
|
||||
val tokens: Flow<PagingData<TokenItemState>>,
|
||||
val isLoading: Boolean,
|
||||
val addCustomTokenButton: AddCustomTokenButton,
|
||||
val chooseWalletState: ChooseWalletState,
|
||||
val derivationNotification: DerivationNotificationState? = null,
|
||||
val selectedToken: TokenItemState.Loaded? = null,
|
||||
val showChooseWalletScreen: Boolean = false,
|
||||
val event: StateEvent<Event>,
|
||||
)
|
||||
|
||||
data class AddCustomTokenButton(
|
||||
val isVisible: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state.previewdata
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData
|
||||
import com.tangem.managetokens.presentation.managetokens.state.AddCustomTokenButton
|
||||
import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.SearchBarState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
internal object ManageTokensStatePreviewData {
|
||||
val loadedState: ManageTokensState
|
||||
get() = ManageTokensState(
|
||||
searchBarState = searchState,
|
||||
tokens = flowOf(PagingData.from(tokens)),
|
||||
isLoading = false,
|
||||
addCustomTokenButton = AddCustomTokenButton(true, {}),
|
||||
derivationNotification = DerivationNotificationStatePreviewData.state,
|
||||
event = consumedEvent(),
|
||||
chooseWalletState = ChooseWalletStatePreviewData.state,
|
||||
)
|
||||
|
||||
val loadingState: ManageTokensState
|
||||
get() = loadedState.copy(isLoading = true)
|
||||
|
||||
private val tokens: List<TokenItemState>
|
||||
get() = listOf(
|
||||
TokenItemStatePreviewData.loadedPriceDown,
|
||||
TokenItemStatePreviewData.loadedPriceUp,
|
||||
)
|
||||
|
||||
private val searchState: SearchBarState
|
||||
get() = SearchBarState(
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
active = false,
|
||||
onActiveChange = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
|
||||
@Composable
|
||||
internal fun ChooseNetworkBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet<ChooseNetworkBottomSheetConfig>(
|
||||
config = config,
|
||||
contentColor = TangemTheme.colors.background.tertiary,
|
||||
) {
|
||||
ChooseNetworkScreen(state = it.selectedToken, walletState = it.chooseWalletState)
|
||||
}
|
||||
}
|
||||
|
||||
internal class ChooseNetworkBottomSheetConfig(
|
||||
val selectedToken: TokenItemState.Loaded,
|
||||
val chooseWalletState: ChooseWalletState,
|
||||
) : TangemBottomSheetConfigContent
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.managetokens.presentation.common.state.AlertState
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheet
|
||||
import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheetConfig
|
||||
import com.tangem.managetokens.presentation.common.ui.EventEffect
|
||||
import com.tangem.managetokens.presentation.common.ui.components.Alert
|
||||
import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.previewdata.ManageTokensStatePreviewData
|
||||
import com.tangem.managetokens.presentation.managetokens.ui.components.DerivationNotification
|
||||
import com.tangem.managetokens.presentation.managetokens.ui.components.TokensList
|
||||
import com.tangem.managetokens.presentation.managetokens.ui.components.TokensSearchBar
|
||||
|
||||
@Composable
|
||||
internal fun ManageTokensScreen(state: ManageTokensState) {
|
||||
var alertState by remember { mutableStateOf<AlertState?>(value = null) }
|
||||
|
||||
EventEffect(
|
||||
event = state.event,
|
||||
onAlertStateSet = { alertState = it },
|
||||
)
|
||||
alertState?.let {
|
||||
Alert(state = it, onDismiss = { alertState = null })
|
||||
}
|
||||
|
||||
Content(state)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: ManageTokensState) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.padding(top = TangemTheme.dimens.spacing32),
|
||||
) {
|
||||
Column {
|
||||
val listState = rememberLazyListState()
|
||||
val raiseSearchBar by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }
|
||||
|
||||
val elevation by animateDpAsState(
|
||||
targetValue = if (raiseSearchBar) {
|
||||
TangemTheme.dimens.elevation8
|
||||
} else {
|
||||
TangemTheme.dimens.elevation0
|
||||
},
|
||||
label = "top_bar_elevation",
|
||||
)
|
||||
Surface(
|
||||
elevation = elevation,
|
||||
modifier = Modifier,
|
||||
) {
|
||||
TokensSearchBar(
|
||||
state = state.searchBarState,
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing20),
|
||||
)
|
||||
}
|
||||
val tokens = state.tokens.collectAsLazyPagingItems()
|
||||
TokensList(tokens = tokens, addCustomTokenButton = state.addCustomTokenButton)
|
||||
}
|
||||
state.derivationNotification?.let {
|
||||
DerivationNotification(
|
||||
config = it.config,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
state.selectedToken?.let { selectedToken ->
|
||||
ManageTokensBottomSheet(selectedToken = selectedToken, state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageTokensBottomSheet(selectedToken: TokenItemState.Loaded, state: ManageTokensState) {
|
||||
if (state.showChooseWalletScreen && state.chooseWalletState is ChooseWalletState.Choose) {
|
||||
val config = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
content = ChooseWalletBottomSheetConfig(state.chooseWalletState),
|
||||
onDismissRequest = state.chooseWalletState.onCloseChoosingWalletClick,
|
||||
)
|
||||
ChooseWalletBottomSheet(config)
|
||||
} else {
|
||||
val config = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
content = ChooseNetworkBottomSheetConfig(
|
||||
selectedToken = selectedToken,
|
||||
chooseWalletState = state.chooseWalletState,
|
||||
),
|
||||
onDismissRequest = selectedToken.chooseNetworkState.onCloseChooseNetworkScreen,
|
||||
)
|
||||
ChooseNetworkBottomSheet(config)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ManageTokensScreen_LightTheme(
|
||||
@PreviewParameter(ManageTokensConfigProvider::class)
|
||||
state: ManageTokensState,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
ManageTokensScreen(state)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ManageTokensScreen_DarkTheme(
|
||||
@PreviewParameter(ManageTokensConfigProvider::class)
|
||||
state: ManageTokensState,
|
||||
) {
|
||||
TangemTheme(isDark = true) {
|
||||
ManageTokensScreen(state)
|
||||
}
|
||||
}
|
||||
|
||||
private class ManageTokensConfigProvider : CollectionPreviewParameterProvider<ManageTokensState>(
|
||||
collection = listOf(
|
||||
ManageTokensStatePreviewData.loadingState,
|
||||
ManageTokensStatePreviewData.loadedState,
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun AddCustomTokenButton(onButtonClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size68)
|
||||
.fillMaxWidth()
|
||||
.clickable { onButtonClick() }
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size36)
|
||||
.background(color = TangemTheme.colors.button.secondary, shape = CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_plus_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
SpacerW(width = TangemTheme.dimens.spacing12)
|
||||
Text(
|
||||
text = stringResource(id = R.string.add_custom_token_title),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AddCustomTokenButton_Preview_Light() {
|
||||
TangemTheme(isDark = false) {
|
||||
AddCustomTokenButton(onButtonClick = { })
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AddCustomTokenButton_Preview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
AddCustomTokenButton(onButtonClick = { })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.managetokens.state.AddCustomTokenButton
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
|
||||
private const val PLACEHOLDER_ITEMS_COUNT = 50
|
||||
|
||||
@Composable
|
||||
internal fun TokensList(tokens: LazyPagingItems<TokenItemState>, addCustomTokenButton: AddCustomTokenButton) {
|
||||
LazyColumn {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
|
||||
if (tokens.loadState.refresh is LoadState.Loading) {
|
||||
items(PLACEHOLDER_ITEMS_COUNT) {
|
||||
TokenRowItem(state = TokenItemState.Loading(it.toString()))
|
||||
}
|
||||
} else {
|
||||
val tokensList = tokens.itemSnapshotList
|
||||
if (tokensList.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_nothing_found),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(items = tokensList.items, key = TokenItemState::id) { token ->
|
||||
TokenRowItem(state = token)
|
||||
}
|
||||
|
||||
if (addCustomTokenButton.isVisible) {
|
||||
item {
|
||||
AddCustomTokenButton(onButtonClick = { addCustomTokenButton.onClick })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,11 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
|
|||
|
||||
import androidx.paging.PagingData
|
||||
import arrow.core.Either
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.common.Provider
|
||||
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.bottomsheets.tokenreceive.mapToAddressModels
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
|
|
@ -17,6 +16,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.NetworkAddress
|
||||
import com.tangem.domain.tokens.model.TokenActionsState
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
|
|
@ -176,7 +176,7 @@ internal class TokenDetailsStateFactory(
|
|||
|
||||
fun getStateWithReceiveBottomSheet(
|
||||
currency: CryptoCurrency,
|
||||
addresses: List<Address>,
|
||||
networkAddress: NetworkAddress,
|
||||
sendCopyAnalyticsEvent: () -> Unit,
|
||||
sendShareAnalyticsEvent: () -> Unit,
|
||||
): TokenDetailsState {
|
||||
|
|
@ -188,12 +188,7 @@ internal class TokenDetailsStateFactory(
|
|||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
network = currency.network.name,
|
||||
addresses = addresses.map {
|
||||
AddressModel(
|
||||
value = it.value,
|
||||
type = AddressModel.Type.valueOf(it.type.name),
|
||||
)
|
||||
},
|
||||
addresses = networkAddress.availableAddresses.mapToAddressModels(currency).toImmutableList(),
|
||||
onCopyClick = sendCopyAnalyticsEvent,
|
||||
onShareClick = sendShareAnalyticsEvent,
|
||||
),
|
||||
|
|
@ -201,20 +196,16 @@ internal class TokenDetailsStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun getStateWithChooseAddressBottomSheet(addresses: List<Address>): TokenDetailsState {
|
||||
fun getStateWithChooseAddressBottomSheet(
|
||||
currency: CryptoCurrency,
|
||||
networkAddress: NetworkAddress,
|
||||
): TokenDetailsState {
|
||||
return currentStateProvider().copy(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
onDismissRequest = clickIntents::onDismissBottomSheet,
|
||||
content = ChooseAddressBottomSheetConfig(
|
||||
addressModels = addresses
|
||||
.map { address ->
|
||||
AddressModel(
|
||||
value = address.value,
|
||||
type = AddressModel.Type.valueOf(address.type.name),
|
||||
)
|
||||
}
|
||||
.toImmutableList(),
|
||||
addressModels = networkAddress.availableAddresses.mapToAddressModels(currency).toImmutableList(),
|
||||
onClick = clickIntents::onAddressTypeSelected,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ import com.tangem.domain.tokens.*
|
|||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.NetworkAddress
|
||||
import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent
|
||||
import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent
|
||||
import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
|
|
@ -367,19 +367,15 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onReceiveClick() {
|
||||
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol))
|
||||
val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return
|
||||
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val addresses = walletManagersFacade.getAddress(
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
|
||||
analyticsEventsHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened)
|
||||
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol))
|
||||
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened)
|
||||
|
||||
uiState = stateFactory.getStateWithReceiveBottomSheet(
|
||||
currency = cryptoCurrency,
|
||||
addresses = addresses,
|
||||
networkAddress = networkAddress,
|
||||
sendCopyAnalyticsEvent = {
|
||||
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol))
|
||||
},
|
||||
|
|
@ -442,22 +438,23 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun openExplorer() {
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val addresses = walletManagersFacade.getAddress(
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
val currencyStatus = cryptoCurrencyStatus ?: return
|
||||
|
||||
if (addresses.size == 1) {
|
||||
router.openUrl(
|
||||
url = getExploreUrlUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
addressType = AddressType.Default,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
uiState = stateFactory.getStateWithChooseAddressBottomSheet(addresses = addresses)
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
when (val addresses = currencyStatus.value.networkAddress) {
|
||||
is NetworkAddress.Selectable -> {
|
||||
uiState = stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses)
|
||||
}
|
||||
is NetworkAddress.Single -> {
|
||||
router.openUrl(
|
||||
url = getExploreUrlUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = cryptoCurrency,
|
||||
addressType = AddressType.Default,
|
||||
),
|
||||
)
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,17 +54,18 @@ internal class DefaultWalletRouter(
|
|||
) {
|
||||
composable(WalletRoute.Wallet.route) {
|
||||
if (walletFeatureToggles.isWalletsScrollingPreviewEnabled) {
|
||||
val viewModel = hiltViewModel<WalletViewModel>().apply {
|
||||
router = this@DefaultWalletRouter
|
||||
}
|
||||
|
||||
WalletScreen(state = viewModel.uiState)
|
||||
} else {
|
||||
val viewModel = hiltViewModel<WalletViewModelV2>().apply {
|
||||
setWalletRouter(router = this@DefaultWalletRouter)
|
||||
}
|
||||
|
||||
WalletScreenV2(state = viewModel.uiState.collectAsStateWithLifecycle().value)
|
||||
} else {
|
||||
val viewModel = hiltViewModel<WalletViewModel>().apply {
|
||||
router = this@DefaultWalletRouter
|
||||
}
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
|
||||
|
||||
WalletScreen(state = viewModel.uiState)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.tangem.core.navigation.AppScreen
|
|||
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.bottomsheets.tokenreceive.mapToAddressModels
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.WrappedList
|
||||
|
|
@ -42,10 +43,7 @@ import com.tangem.domain.settings.*
|
|||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.NetworkGroup
|
||||
import com.tangem.domain.tokens.model.TokenList
|
||||
import com.tangem.domain.tokens.model.*
|
||||
import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent
|
||||
import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
|
|
@ -689,20 +687,11 @@ internal class WalletViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {
|
||||
val state = uiState as? WalletState.ContentState ?: return
|
||||
|
||||
analyticsEventsHandler.send(
|
||||
event = TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrencyStatus.currency.symbol),
|
||||
)
|
||||
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex)
|
||||
|
||||
val addresses = walletManagersFacade.getAddress(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
)
|
||||
|
||||
analyticsEventsHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened)
|
||||
|
||||
val currency = cryptoCurrencyStatus.currency
|
||||
|
|
@ -711,12 +700,11 @@ internal class WalletViewModel @Inject constructor(
|
|||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
network = currency.network.name,
|
||||
addresses = addresses.map {
|
||||
AddressModel(
|
||||
value = it.value,
|
||||
type = AddressModel.Type.valueOf(it.type.name),
|
||||
)
|
||||
},
|
||||
addresses = cryptoCurrencyStatus.value.networkAddress
|
||||
?.availableAddresses
|
||||
?.mapToAddressModels(currency)
|
||||
.orEmpty()
|
||||
.toImmutableList(),
|
||||
onCopyClick = {
|
||||
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol))
|
||||
},
|
||||
|
|
@ -821,41 +809,39 @@ internal class WalletViewModel @Inject constructor(
|
|||
|
||||
private fun openExplorer() {
|
||||
val state = uiState as? WalletState.ContentState ?: return
|
||||
val currency = singleWalletCryptoCurrencyStatus?.currency ?: return
|
||||
val currencyStatus = singleWalletCryptoCurrencyStatus ?: return
|
||||
val currency = currencyStatus.currency
|
||||
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
val userWalletId = getWallet(state.walletsListConfig.selectedWalletIndex).walletId
|
||||
|
||||
val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network)
|
||||
|
||||
if (addresses.size == 1) {
|
||||
router.openUrl(
|
||||
url = getExploreUrlUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = currency,
|
||||
addressType = AddressType.Default,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
uiState = stateFactory.getStateWithOpenWalletBottomSheet(
|
||||
ChooseAddressBottomSheetConfig(
|
||||
addressModels = addresses
|
||||
.map { address ->
|
||||
AddressModel(
|
||||
value = address.value,
|
||||
type = AddressModel.Type.valueOf(address.type.name),
|
||||
when (val addresses = currencyStatus.value.networkAddress) {
|
||||
is NetworkAddress.Selectable -> {
|
||||
uiState = stateFactory.getStateWithOpenWalletBottomSheet(
|
||||
ChooseAddressBottomSheetConfig(
|
||||
addressModels = addresses.availableAddresses
|
||||
.mapToAddressModels(currency)
|
||||
.toImmutableList(),
|
||||
onClick = {
|
||||
onAddressTypeSelected(
|
||||
userWalletId = userWalletId,
|
||||
currency = currency,
|
||||
addressModel = it,
|
||||
)
|
||||
}
|
||||
.toImmutableList(),
|
||||
onClick = {
|
||||
onAddressTypeSelected(
|
||||
userWalletId = userWalletId,
|
||||
currency = currency,
|
||||
addressModel = it,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
is NetworkAddress.Single -> {
|
||||
router.openUrl(
|
||||
url = getExploreUrlUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = currency,
|
||||
addressType = AddressType.Default,
|
||||
),
|
||||
)
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
|
||||
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
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.bottomsheets.tokenreceive.mapToAddressModels
|
||||
import com.tangem.core.ui.extensions.WrappedList
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
|
|
@ -20,6 +20,7 @@ import com.tangem.domain.tokens.RemoveCurrencyUseCase
|
|||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.NetworkAddress
|
||||
import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent
|
||||
import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent
|
||||
import com.tangem.domain.walletconnect.WalletConnectActions
|
||||
|
|
@ -141,15 +142,15 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
|||
)
|
||||
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
val currency = cryptoCurrencyStatus.currency
|
||||
val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network)
|
||||
|
||||
analyticsEventHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened)
|
||||
|
||||
stateHolder.update(
|
||||
OpenBottomSheetTransformer(
|
||||
userWalletId = userWalletId,
|
||||
content = createReceiveBottomSheetContent(currency, addresses),
|
||||
content = createReceiveBottomSheetContent(
|
||||
currency = cryptoCurrencyStatus.currency,
|
||||
addresses = cryptoCurrencyStatus.value.networkAddress?.availableAddresses ?: return@launch,
|
||||
),
|
||||
onDismissBottomSheet = {
|
||||
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId))
|
||||
},
|
||||
|
|
@ -160,18 +161,13 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
|||
|
||||
private fun createReceiveBottomSheetContent(
|
||||
currency: CryptoCurrency,
|
||||
addresses: List<Address>,
|
||||
addresses: Set<NetworkAddress.Address>,
|
||||
): TangemBottomSheetConfigContent {
|
||||
return TokenReceiveBottomSheetConfig(
|
||||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
network = currency.network.name,
|
||||
addresses = addresses.map { address ->
|
||||
AddressModel(
|
||||
value = address.value,
|
||||
type = AddressModel.Type.valueOf(address.type.name),
|
||||
)
|
||||
},
|
||||
addresses = addresses.mapToAddressModels(currency).toImmutableList(),
|
||||
onCopyClick = {
|
||||
analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol))
|
||||
},
|
||||
|
|
@ -329,40 +325,36 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
|||
val userWalletId = stateHolder.getSelectedWalletId()
|
||||
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
val currency = getPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId)?.currency ?: return@launch
|
||||
val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network)
|
||||
val currencyStatus = getPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId) ?: return@launch
|
||||
|
||||
if (addresses.size == 1) {
|
||||
router.openUrl(
|
||||
url = getExploreUrlUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = currency,
|
||||
addressType = AddressType.Default,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
showChooseAddressBottomSheet(userWalletId, addresses, currency)
|
||||
when (val addresses = currencyStatus.value.networkAddress) {
|
||||
is NetworkAddress.Selectable -> {
|
||||
showChooseAddressBottomSheet(userWalletId, addresses.availableAddresses, currencyStatus.currency)
|
||||
}
|
||||
is NetworkAddress.Single -> {
|
||||
router.openUrl(
|
||||
url = getExploreUrlUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currency = currencyStatus.currency,
|
||||
addressType = AddressType.Default,
|
||||
),
|
||||
)
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showChooseAddressBottomSheet(
|
||||
userWalletId: UserWalletId,
|
||||
addresses: List<Address>,
|
||||
addresses: Set<NetworkAddress.Address>,
|
||||
currency: CryptoCurrency,
|
||||
) {
|
||||
stateHolder.update(
|
||||
OpenBottomSheetTransformer(
|
||||
userWalletId = userWalletId,
|
||||
content = ChooseAddressBottomSheetConfig(
|
||||
addressModels = addresses
|
||||
.map { address ->
|
||||
AddressModel(
|
||||
value = address.value,
|
||||
type = AddressModel.Type.valueOf(address.type.name),
|
||||
)
|
||||
}
|
||||
.toImmutableList(),
|
||||
addressModels = addresses.mapToAddressModels(currency).toImmutableList(),
|
||||
onClick = {
|
||||
onAddressTypeSelected(
|
||||
userWalletId = userWalletId,
|
||||
|
|
|
|||
|
|
@ -88,10 +88,10 @@ spr-client = "3.6.2"
|
|||
# endregion Other libraries
|
||||
|
||||
# region Tangem
|
||||
tangemBlockchainSdk = "develop-396"
|
||||
tangemBlockchainSdk = "develop-401"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "develop-312"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
# endregion Tangem
|
||||
|
||||
# region Tools
|
||||
|
|
|
|||
|
|
@ -128,7 +128,6 @@ include(":domain:app-theme:models")
|
|||
include(":domain:balance-hiding")
|
||||
include(":domain:balance-hiding:models")
|
||||
include(":domain:transaction")
|
||||
|
||||
// endregion Domain modules
|
||||
|
||||
// region Data modules
|
||||
|
|
@ -142,4 +141,5 @@ include(":data:source:preferences")
|
|||
include(":data:settings")
|
||||
include(":data:txhistory")
|
||||
include(":data:wallets")
|
||||
include(":data:analytics")
|
||||
// endregion Data modules
|
||||
Loading…
Add table
Add a link
Reference in a new issue