Updated on 2026-08-14
This commit is contained in:
commit
512ab6d54d
206 changed files with 8933 additions and 630 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) {
|
||||
|
|
|
|||
|
|
@ -91,4 +91,16 @@ internal object SettingsDomainModule {
|
|||
): UpdateBalanceHidingSettingsUseCase {
|
||||
return UpdateBalanceHidingSettingsUseCase(balanceHidingRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideSetWalletsScrollPreviewIsShown(settingsRepository: SettingsRepository): NeverToShowWalletsScrollPreview {
|
||||
return NeverToShowWalletsScrollPreview(settingsRepository = settingsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideIsWalletsScrollPreviewEnabled(settingsRepository: SettingsRepository): IsWalletsScrollPreviewEnabled {
|
||||
return IsWalletsScrollPreviewEnabled(settingsRepository = settingsRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -15,10 +18,25 @@ internal object TransactionDomainModule {
|
|||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetUseCase(
|
||||
fun provideGetFeeUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetFeeUseCase {
|
||||
return GetFeeUseCase(walletManagersFacade, dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideSendTransactionUseCase(
|
||||
isDemoCardUseCase: IsDemoCardUseCase,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
): SendTransactionUseCase {
|
||||
return SendTransactionUseCase(
|
||||
isDemoCardUseCase = isDemoCardUseCase,
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
@ -63,6 +63,7 @@ internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier
|
|||
checked = item.isChecked,
|
||||
enabled = item.isEnabled,
|
||||
onCheckedChange = item.onCheckedChange,
|
||||
checkedColor = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
|||
import com.tangem.domain.tokens.TokenWithBlockchain
|
||||
import com.tangem.domain.tokens.TokensAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.walletconnect.WalletConnectActions
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.tap.*
|
||||
|
|
@ -90,13 +92,17 @@ object TokensMiddleware {
|
|||
if (scanResponse.supportsHdWallet()) {
|
||||
deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) {
|
||||
submitNewAdd(
|
||||
userWalletId = action.userWallet.walletId,
|
||||
userWallet = action.userWallet,
|
||||
updatedScanResponse = it,
|
||||
currencyList = currencyList,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
submitNewAdd(userWalletId = action.userWallet.walletId, scanResponse, currencyList = currencyList)
|
||||
submitNewAdd(
|
||||
userWallet = action.userWallet,
|
||||
updatedScanResponse = scanResponse,
|
||||
currencyList = currencyList,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -375,16 +381,18 @@ object TokensMiddleware {
|
|||
}
|
||||
|
||||
private fun submitNewAdd(
|
||||
userWalletId: UserWalletId,
|
||||
userWallet: UserWallet,
|
||||
updatedScanResponse: ScanResponse,
|
||||
currencyList: List<CryptoCurrency>,
|
||||
) {
|
||||
scope.launch {
|
||||
userWalletsListManager.update(
|
||||
userWalletId = userWalletId,
|
||||
userWalletId = userWallet.walletId,
|
||||
update = { it.copy(scanResponse = updatedScanResponse) },
|
||||
).doOnSuccess {
|
||||
addCryptoCurrenciesUseCase(userWalletId, currencyList)
|
||||
addCryptoCurrenciesUseCase(userWallet.walletId, currencyList).onRight {
|
||||
store.dispatch(action = WalletConnectActions.New.SetupUserChains(userWallet = userWallet))
|
||||
}
|
||||
}
|
||||
}
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -32,6 +32,10 @@ object PreferencesKeys {
|
|||
val SELECTED_APP_CURRENCY_KEY by lazy { stringPreferencesKey(name = "selectedAppCurrency") }
|
||||
|
||||
val BALANCE_HIDING_SETTINGS_KEY by lazy { stringPreferencesKey(name = "balanceHidingSettings") }
|
||||
|
||||
val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") }
|
||||
|
||||
val SENT_ONE_TIME_EVENTS_KEY by lazy { stringPreferencesKey(name = "sentOneTimeEvents") }
|
||||
}
|
||||
|
||||
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */
|
||||
|
|
|
|||
|
|
@ -34,5 +34,9 @@
|
|||
{
|
||||
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "WALLETS_SCROLLING_PREVIEW_ENABLED",
|
||||
"version": "5.4.0"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import androidx.compose.material.Text
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
|
@ -162,11 +163,12 @@ fun CardWithIcon(
|
|||
* >Figma component</a>
|
||||
*/
|
||||
@Composable
|
||||
fun IconWithTitleAndDescription(
|
||||
internal fun IconWithTitleAndDescription(
|
||||
title: String,
|
||||
description: String,
|
||||
description: String?,
|
||||
icon: @Composable () -> Unit,
|
||||
additionalContent: @Composable () -> Unit = {},
|
||||
iconBackground: Color = TangemTheme.colors.background.secondary,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
@ -182,7 +184,7 @@ fun IconWithTitleAndDescription(
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.secondary,
|
||||
color = iconBackground,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.height(TangemTheme.dimens.size40)
|
||||
|
|
@ -205,12 +207,14 @@ fun IconWithTitleAndDescription(
|
|||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
SpacerH4()
|
||||
Text(
|
||||
text = description,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
if (description != null) {
|
||||
SpacerH4()
|
||||
Text(
|
||||
text = description,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
additionalContent()
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
@Composable
|
||||
fun TangemSwitch(
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
checkedColor: Color = TangemTheme.colors.icon.accent,
|
||||
checkedColor: Color = TangemTheme.colors.control.checked,
|
||||
uncheckedColor: Color = TangemTheme.colors.icon.informative,
|
||||
size: Dp = 48.dp,
|
||||
checked: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.material.ExperimentalMaterialApi
|
|||
import androidx.compose.material.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
|
|
@ -36,6 +37,28 @@ fun WarningCard(title: String, description: String, icon: @Composable (() -> Uni
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A card with a warning icon to the left and title without description shown to the right of it.
|
||||
*
|
||||
* @param title title of the warning in bold
|
||||
*
|
||||
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=290%3A217&t=yMepJZTRh5bLkOoJ-1"
|
||||
* >Figma component</a>
|
||||
*/
|
||||
@Composable
|
||||
fun WarningCardTitleOnly(title: String, icon: @Composable (() -> Unit)? = null) {
|
||||
WarningCardMaterial3Style(
|
||||
content = {
|
||||
WarningBody(
|
||||
title = title,
|
||||
description = null,
|
||||
icon = icon,
|
||||
iconBackground = TangemTheme.colors.button.disabled,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* [WarningCard], but clickable (with an 'greater then' icon to the left)
|
||||
*
|
||||
|
|
@ -105,7 +128,8 @@ fun RefreshableWaringCard(
|
|||
@Composable
|
||||
private fun WarningBody(
|
||||
title: String,
|
||||
description: String,
|
||||
description: String?,
|
||||
iconBackground: Color = TangemTheme.colors.background.secondary,
|
||||
icon: @Composable (() -> Unit)? = null,
|
||||
additionalContent: @Composable () -> Unit = {},
|
||||
) {
|
||||
|
|
@ -119,6 +143,7 @@ private fun WarningBody(
|
|||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
iconBackground = iconBackground,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -136,6 +161,20 @@ private fun WarningCardSurface(onClick: (() -> Unit)? = null, content: @Composab
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun WarningCardMaterial3Style(onClick: (() -> Unit)? = null, content: @Composable () -> Unit) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius16),
|
||||
backgroundColor = TangemTheme.colors.button.disabled,
|
||||
elevation = TangemTheme.dimens.elevation0,
|
||||
onClick = onClick ?: {},
|
||||
enabled = onClick != null,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
// endregion elements
|
||||
|
||||
// region Preview
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -39,7 +42,10 @@ fun AppBarWithBackButtonAndIcon(
|
|||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size24)
|
||||
.clickable { onBackClick() },
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
) { onBackClick() },
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
AnimatedContent(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import androidx.compose.material3.ModalBottomSheet
|
|||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -20,6 +21,7 @@ import kotlinx.coroutines.launch
|
|||
@Composable
|
||||
inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
||||
config: TangemBottomSheetConfig,
|
||||
contentColor: Color = TangemTheme.colors.background.primary,
|
||||
crossinline content: @Composable ColumnScope.(T) -> Unit,
|
||||
) {
|
||||
var isVisible by remember { mutableStateOf(value = config.isShow) }
|
||||
|
|
@ -29,7 +31,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
|||
ModalBottomSheet(
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
containerColor = contentColor,
|
||||
shape = TangemTheme.shapes.bottomSheetLarge,
|
||||
dragHandle = { TangemBottomSheetDraggableHeader() },
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.core.ui.components.fields
|
||||
package com.tangem.core.ui.components.fields.visualtransformations
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.tangem.core.ui.components.transactions
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.toDateFormat
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
|
||||
/**
|
||||
* Common transaction done screen title
|
||||
*
|
||||
* @param titleRes title resource
|
||||
* @param date transaction timestamp in millis
|
||||
*/
|
||||
@Composable
|
||||
fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_empty_in_process_64),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8)
|
||||
.size(TangemTheme.dimens.size64),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = titleRes),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing32),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_date_format, date.toDateFormat(), date.toTimeFormat()),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing4),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Previews
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TransactionDoneTitlePreview_Light() {
|
||||
TangemTheme {
|
||||
TransactionDoneTitle(
|
||||
titleRes = R.string.sent_transaction_sent_title,
|
||||
date = 0,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TransactionDoneTitlePreview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
TransactionDoneTitle(
|
||||
titleRes = R.string.sent_transaction_sent_title,
|
||||
date = 0,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -7,18 +7,34 @@ import androidx.compose.ui.composed
|
|||
import androidx.compose.ui.draw.clip
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modifier = composed {
|
||||
val modifierWithHorizontalPadding = this.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
fun Modifier.roundedShapeItemDecoration(
|
||||
currentIndex: Int,
|
||||
lastIndex: Int,
|
||||
addDefaultPadding: Boolean = true,
|
||||
): Modifier = composed {
|
||||
val modifier = if (addDefaultPadding) this.padding(horizontal = TangemTheme.dimens.spacing16) else this
|
||||
val isSingleItem = currentIndex == 0 && lastIndex == 0
|
||||
when {
|
||||
isSingleItem -> {
|
||||
modifierWithHorizontalPadding
|
||||
.padding(top = TangemTheme.dimens.spacing14)
|
||||
modifier
|
||||
.then(
|
||||
if (addDefaultPadding) {
|
||||
Modifier.padding(top = TangemTheme.dimens.spacing14)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
|
||||
}
|
||||
currentIndex == 0 -> {
|
||||
modifierWithHorizontalPadding
|
||||
.padding(top = TangemTheme.dimens.spacing14)
|
||||
modifier
|
||||
.then(
|
||||
if (addDefaultPadding) {
|
||||
Modifier.padding(top = TangemTheme.dimens.spacing14)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.clip(
|
||||
shape = RoundedCornerShape(
|
||||
topStart = TangemTheme.dimens.radius16,
|
||||
|
|
@ -27,7 +43,7 @@ fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modi
|
|||
)
|
||||
}
|
||||
currentIndex == lastIndex -> {
|
||||
modifierWithHorizontalPadding
|
||||
modifier
|
||||
.clip(
|
||||
shape = RoundedCornerShape(
|
||||
bottomStart = TangemTheme.dimens.radius16,
|
||||
|
|
@ -35,6 +51,6 @@ fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modi
|
|||
),
|
||||
)
|
||||
}
|
||||
else -> modifierWithHorizontalPadding
|
||||
else -> modifier
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.core.ui.event
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.NonRestartableComposable
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* A Composable function that reacts to a given [StateEvent], executing the provided action only once when the event
|
||||
|
|
@ -17,8 +18,8 @@ import androidx.compose.runtime.NonRestartableComposable
|
|||
fun <A> EventEffect(event: StateEvent<A>, onTrigger: suspend (data: A) -> Unit) {
|
||||
LaunchedEffect(event) {
|
||||
if (event is StateEvent.Triggered<A>) {
|
||||
onTrigger(event.data)
|
||||
event.onConsume()
|
||||
launch { onTrigger(event.data) }
|
||||
.invokeOnCompletion { event.onConsume() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,5 +1,6 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
|
|
@ -24,6 +25,10 @@ object BigDecimalFormatter {
|
|||
return formatter.format(cryptoAmount) + "\u2009$cryptoCurrency"
|
||||
}
|
||||
|
||||
fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency): String {
|
||||
return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals)
|
||||
}
|
||||
|
||||
fun formatFiatAmount(
|
||||
fiatAmount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
|
|
|
|||
10
core/ui/src/main/res/drawable/ic_empty_in_process_64.xml
Normal file
10
core/ui/src/main/res/drawable/ic_empty_in_process_64.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64">
|
||||
<path
|
||||
android:pathData="M28.384,0.616C27.896,0.128 27.104,0.128 26.616,0.616C26.128,1.104 26.128,1.896 26.616,2.384L31.002,6.769C17.519,7.294 6.75,18.389 6.75,32C6.75,45.945 18.055,57.25 32,57.25C45.945,57.25 57.25,45.945 57.25,32C57.25,31.31 56.69,30.75 56,30.75C55.31,30.75 54.75,31.31 54.75,32C54.75,44.564 44.564,54.75 32,54.75C19.435,54.75 9.25,44.564 9.25,32C9.25,19.785 18.878,9.818 30.959,9.273L26.616,13.616C26.128,14.104 26.128,14.896 26.616,15.384C27.104,15.872 27.896,15.872 28.384,15.384L34.884,8.884C35.372,8.396 35.372,7.604 34.884,7.116L28.384,0.616ZM39,22.75H40.25V24V26V26.518L39.884,26.884L33.768,33L39.884,39.116L40.25,39.482V40V42V43.25H39H25H23.75V42V40V39.482L24.116,39.116L30.232,33L24.116,26.884L23.75,26.518V26V24V22.75H25H39ZM26.25,25.482L32,31.232L37.75,25.482V25.25H26.25V25.482ZM37.75,40.518L32,34.768L26.25,40.518V40.75H37.75V40.518Z"
|
||||
android:fillColor="#C9C9C9"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
13
core/ui/src/main/res/drawable/ic_web_24.xml
Normal file
13
core/ui/src/main/res/drawable/ic_web_24.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
<path
|
||||
android:pathData="M12,22C10.633,22 9.342,21.737 8.125,21.212C6.908,20.688 5.846,19.971 4.938,19.063C4.029,18.154 3.313,17.092 2.787,15.875C2.263,14.658 2,13.367 2,12C2,10.617 2.263,9.321 2.787,8.113C3.313,6.904 4.029,5.846 4.938,4.938C5.846,4.029 6.908,3.313 8.125,2.787C9.342,2.263 10.633,2 12,2C13.383,2 14.679,2.263 15.887,2.787C17.096,3.313 18.154,4.029 19.063,4.938C19.971,5.846 20.688,6.904 21.212,8.113C21.737,9.321 22,10.617 22,12C22,13.367 21.737,14.658 21.212,15.875C20.688,17.092 19.971,18.154 19.063,19.063C18.154,19.971 17.096,20.688 15.887,21.212C14.679,21.737 13.383,22 12,22ZM12,19.95C12.433,19.35 12.808,18.725 13.125,18.075C13.442,17.425 13.7,16.733 13.9,16H10.1C10.3,16.733 10.558,17.425 10.875,18.075C11.192,18.725 11.567,19.35 12,19.95ZM9.4,19.55C9.1,19 8.837,18.429 8.613,17.837C8.387,17.246 8.2,16.633 8.05,16H5.1C5.583,16.833 6.188,17.558 6.912,18.175C7.637,18.792 8.467,19.25 9.4,19.55ZM14.6,19.55C15.533,19.25 16.362,18.792 17.087,18.175C17.813,17.558 18.417,16.833 18.9,16H15.95C15.8,16.633 15.613,17.246 15.387,17.837C15.163,18.429 14.9,19 14.6,19.55ZM4.25,14H7.65C7.6,13.667 7.563,13.337 7.537,13.012C7.512,12.688 7.5,12.35 7.5,12C7.5,11.65 7.512,11.313 7.537,10.988C7.563,10.663 7.6,10.333 7.65,10H4.25C4.167,10.333 4.104,10.663 4.063,10.988C4.021,11.313 4,11.65 4,12C4,12.35 4.021,12.688 4.063,13.012C4.104,13.337 4.167,13.667 4.25,14ZM9.65,14H14.35C14.4,13.667 14.438,13.337 14.462,13.012C14.488,12.688 14.5,12.35 14.5,12C14.5,11.65 14.488,11.313 14.462,10.988C14.438,10.663 14.4,10.333 14.35,10H9.65C9.6,10.333 9.563,10.663 9.538,10.988C9.512,11.313 9.5,11.65 9.5,12C9.5,12.35 9.512,12.688 9.538,13.012C9.563,13.337 9.6,13.667 9.65,14ZM16.35,14H19.75C19.833,13.667 19.896,13.337 19.938,13.012C19.979,12.688 20,12.35 20,12C20,11.65 19.979,11.313 19.938,10.988C19.896,10.663 19.833,10.333 19.75,10H16.35C16.4,10.333 16.438,10.663 16.462,10.988C16.487,11.313 16.5,11.65 16.5,12C16.5,12.35 16.487,12.688 16.462,13.012C16.438,13.337 16.4,13.667 16.35,14ZM15.95,8H18.9C18.417,7.167 17.813,6.442 17.087,5.825C16.362,5.208 15.533,4.75 14.6,4.45C14.9,5 15.163,5.571 15.387,6.162C15.613,6.754 15.8,7.367 15.95,8ZM10.1,8H13.9C13.7,7.267 13.442,6.575 13.125,5.925C12.808,5.275 12.433,4.65 12,4.05C11.567,4.65 11.192,5.275 10.875,5.925C10.558,6.575 10.3,7.267 10.1,8ZM5.1,8H8.05C8.2,7.367 8.387,6.754 8.613,6.162C8.837,5.571 9.1,5 9.4,4.45C8.467,4.75 7.637,5.208 6.912,5.825C6.188,6.442 5.583,7.167 5.1,8Z"
|
||||
android:fillColor="#1E1E1E"/>
|
||||
</group>
|
||||
</vector>
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,10 @@ android {
|
|||
dependencies {
|
||||
implementation(deps.androidx.datastore)
|
||||
|
||||
implementation(deps.tangem.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.data.card
|
||||
|
||||
import com.tangem.TangemSdk
|
||||
import com.tangem.blockchain.common.CommonSigner
|
||||
import com.tangem.common.UserCodeType
|
||||
import com.tangem.common.core.CardIdDisplayFormat
|
||||
import com.tangem.common.core.UserCodeRequestPolicy
|
||||
|
|
@ -57,4 +58,16 @@ internal class DefaultCardSdkConfigRepository(
|
|||
}
|
||||
|
||||
override fun isAccessCodeSavingEnabled(): Boolean = preferencesDataSource.shouldSaveAccessCodes
|
||||
|
||||
override fun getCommonSigner(cardId: String?) = CommonSigner(
|
||||
tangemSdk = sdk,
|
||||
cardId = cardId,
|
||||
initialMessage = null,
|
||||
)
|
||||
|
||||
override fun isLinkedTerminal() = sdk.config.linkedTerminal
|
||||
|
||||
override fun setLinkedTerminal(isLinked: Boolean?) {
|
||||
sdk.config.linkedTerminal = isLinked
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,35 @@
|
|||
package com.tangem.data.settings
|
||||
|
||||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultSettingsRepository(
|
||||
private val preferencesDataSource: PreferencesDataSource,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : SettingsRepository {
|
||||
|
||||
override suspend fun shouldShowSaveUserWalletScreen(): Boolean {
|
||||
return withContext(dispatchers.io) { preferencesDataSource.shouldShowSaveUserWalletScreen }
|
||||
}
|
||||
|
||||
override suspend fun isWalletScrollPreviewEnabled(): Boolean {
|
||||
return appPreferencesStore.getSyncOrDefault(
|
||||
key = PreferencesKeys.WALLETS_SCROLL_PREVIEW_KEY,
|
||||
default = true,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) {
|
||||
appPreferencesStore.store(
|
||||
key = PreferencesKeys.WALLETS_SCROLL_PREVIEW_KEY,
|
||||
value = isEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -21,10 +21,12 @@ internal object SettingsDataModule {
|
|||
@Singleton
|
||||
fun provideSettingsRepository(
|
||||
preferencesDataSource: PreferencesDataSource,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): SettingsRepository {
|
||||
return DefaultSettingsRepository(
|
||||
preferencesDataSource = preferencesDataSource,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,4 +45,5 @@ dependencies {
|
|||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.retrofit) // For HttpException
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
}
|
||||
|
|
@ -1,10 +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.data.tokens.repository.*
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.network.NetworksStatusesStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
@ -12,10 +9,7 @@ import com.tangem.datasource.local.quote.QuotesStore
|
|||
import com.tangem.datasource.local.token.UserMarketCoinsStore
|
||||
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
|
||||
|
|
@ -93,4 +87,18 @@ internal object TokensDataModule {
|
|||
): MarketCryptoCurrencyRepository {
|
||||
return DefaultMarketCryptoCurrencyRepository(userMarketCoinsStore)
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,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
|
||||
|
|
|
|||
|
|
@ -19,4 +19,8 @@ dependencies {
|
|||
|
||||
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.tangem.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.card.repository
|
||||
|
||||
import com.tangem.TangemSdk
|
||||
import com.tangem.blockchain.common.CommonSigner
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
|
||||
/**
|
||||
|
|
@ -28,4 +29,13 @@ interface CardSdkConfigRepository {
|
|||
|
||||
/** Check if access code saving is enabled */
|
||||
fun isAccessCodeSavingEnabled(): Boolean
|
||||
|
||||
/** Get common signer by [cardId] */
|
||||
fun getCommonSigner(cardId: String?): CommonSigner
|
||||
|
||||
/** Check if linked terminal is enabled */
|
||||
fun isLinkedTerminal(): Boolean?
|
||||
|
||||
/** Set linked terminal by [isLinked] */
|
||||
fun setLinkedTerminal(isLinked: Boolean?)
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import com.tangem.blockchain.blockchains.solana.RentProvider
|
|||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
|
|
@ -35,6 +36,7 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.util.EnumSet
|
||||
|
||||
@Suppress("LargeClass")
|
||||
// FIXME: Move to its own module and make internal
|
||||
|
|
@ -431,6 +433,58 @@ class DefaultWalletManagersFacade(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun validateTransaction(
|
||||
amount: Amount,
|
||||
fee: Amount?,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): EnumSet<TransactionError>? {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
return walletManager?.validateTransaction(amount, fee)
|
||||
}
|
||||
|
||||
override suspend fun createTransaction(
|
||||
amount: Amount,
|
||||
fee: Fee,
|
||||
memo: String?,
|
||||
destination: String,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): TransactionData? {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
|
||||
val txData = walletManager?.createTransaction(amount, fee, destination)?.copy(
|
||||
extras = null, // todo add memo [[REDACTED_JIRA]]
|
||||
)
|
||||
|
||||
return txData
|
||||
}
|
||||
|
||||
override suspend fun sendTransaction(
|
||||
txData: TransactionData,
|
||||
signer: CommonSigner,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): SimpleResult {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
return (walletManager as TransactionSender).send(txData, signer)
|
||||
}
|
||||
|
||||
private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set<CryptoCurrency.Token>) {
|
||||
if (tokens.isEmpty()) return
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ package com.tangem.domain.walletmanager
|
|||
|
||||
import arrow.core.Either
|
||||
import com.tangem.blockchain.blockchains.solana.RentProvider
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.common.address.Address
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
|
|
@ -19,6 +19,7 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult
|
|||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.math.BigDecimal
|
||||
import java.util.EnumSet
|
||||
|
||||
// TODO: Move to its own module
|
||||
/**
|
||||
|
|
@ -164,4 +165,57 @@ interface WalletManagersFacade {
|
|||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): Result<TransactionFee>?
|
||||
|
||||
/**
|
||||
* Validates transaction
|
||||
*
|
||||
* @param amount of transaction
|
||||
* @param fee of transaction
|
||||
* @param userWalletId selected wallet id
|
||||
* @param network network of currency
|
||||
*/
|
||||
@Deprecated("Will be removed in future")
|
||||
suspend fun validateTransaction(
|
||||
amount: Amount,
|
||||
fee: Amount?,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): EnumSet<TransactionError>?
|
||||
|
||||
/**
|
||||
* Creates transaction [TransactionData]
|
||||
*
|
||||
* @param amount of transaction
|
||||
* @param fee of transaction
|
||||
* @param memo of transaction optional
|
||||
* @param destination address
|
||||
* @param userWalletId selected wallet id
|
||||
* @param network network of currency
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Deprecated("Will be removed in future")
|
||||
suspend fun createTransaction(
|
||||
amount: Amount,
|
||||
fee: Fee,
|
||||
memo: String?,
|
||||
destination: String,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): TransactionData?
|
||||
|
||||
/**
|
||||
* Sends transaction
|
||||
*
|
||||
* @param txData transaction data
|
||||
* @param signer card signer
|
||||
* @param userWalletId selected wallet id
|
||||
* @param network network of currency
|
||||
*/
|
||||
@Deprecated("Will be removed in future")
|
||||
suspend fun sendTransaction(
|
||||
txData: TransactionData,
|
||||
signer: CommonSigner,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): SimpleResult
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.settings
|
||||
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
|
||||
/**
|
||||
* Checks if wallets scroll preview is enabled
|
||||
*
|
||||
* @property settingsRepository settings repository
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class IsWalletsScrollPreviewEnabled(private val settingsRepository: SettingsRepository) {
|
||||
|
||||
suspend operator fun invoke(): Boolean = settingsRepository.isWalletScrollPreviewEnabled()
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.domain.settings
|
||||
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
|
||||
/**
|
||||
* Never to show wallets scroll preview
|
||||
*
|
||||
* @property settingsRepository settings repository
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class NeverToShowWalletsScrollPreview(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke() = settingsRepository.setWalletScrollPreviewAvailability(isEnabled = false)
|
||||
}
|
||||
|
|
@ -3,4 +3,8 @@ package com.tangem.domain.settings.repositories
|
|||
interface SettingsRepository {
|
||||
|
||||
suspend fun shouldShowSaveUserWalletScreen(): Boolean
|
||||
|
||||
suspend fun isWalletScrollPreviewEnabled(): Boolean
|
||||
|
||||
suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -12,4 +12,7 @@ android {
|
|||
dependencies {
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(deps.tangem.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
}
|
||||
|
|
@ -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,24 @@
|
|||
package com.tangem.domain.tokens.utils
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import java.math.BigDecimal
|
||||
|
||||
/** Converts `BigDecimal` [cryptoCurrency] to [Amount] */
|
||||
fun BigDecimal.convertToAmount(cryptoCurrency: CryptoCurrency) = Amount(
|
||||
currencySymbol = cryptoCurrency.symbol,
|
||||
value = this,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
type = when (cryptoCurrency) {
|
||||
is CryptoCurrency.Coin -> AmountType.Coin
|
||||
is CryptoCurrency.Token -> AmountType.Token(
|
||||
token = Token(
|
||||
symbol = cryptoCurrency.symbol,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -32,10 +32,7 @@ class GetCryptoCurrencyActionsUseCase(
|
|||
) {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend operator fun invoke(
|
||||
userWallet: UserWallet,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
): Flow<TokenActionsState> {
|
||||
operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow<TokenActionsState> {
|
||||
val operations = CurrenciesStatusesOperations(
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
|
|
@ -43,19 +40,25 @@ class GetCryptoCurrencyActionsUseCase(
|
|||
userWalletId = userWallet.walletId,
|
||||
)
|
||||
val networkId = cryptoCurrencyStatus.currency.network.id
|
||||
val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
|
||||
operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId)
|
||||
} else if (!userWallet.isMultiCurrency) {
|
||||
operations.getPrimaryCurrencyStatusFlow()
|
||||
} else {
|
||||
operations.getNetworkCoinFlow(networkId, cryptoCurrencyStatus.currency.network.derivationPath)
|
||||
}
|
||||
return networkFlow.mapLatest { maybeCoinStatus ->
|
||||
createTokenActionsState(
|
||||
userWalletId = userWallet.walletId,
|
||||
coinStatus = maybeCoinStatus.getOrNull(),
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
)
|
||||
|
||||
return flow {
|
||||
val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
|
||||
operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId)
|
||||
} else if (!userWallet.isMultiCurrency) {
|
||||
operations.getPrimaryCurrencyStatusFlow()
|
||||
} else {
|
||||
operations.getNetworkCoinFlow(networkId, cryptoCurrencyStatus.currency.network.derivationPath)
|
||||
}
|
||||
|
||||
val flow = networkFlow.mapLatest { maybeCoinStatus ->
|
||||
createTokenActionsState(
|
||||
userWalletId = userWallet.walletId,
|
||||
coinStatus = maybeCoinStatus.getOrNull(),
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
)
|
||||
}
|
||||
|
||||
emitAll(flow)
|
||||
}.flowOn(dispatchers.io)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>>
|
||||
}
|
||||
|
|
@ -14,10 +14,15 @@ dependencies {
|
|||
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Tangem SDKs */
|
||||
implementation(deps.tangem.card.core)
|
||||
implementation(deps.tangem.blockchain)
|
||||
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.demo)
|
||||
implementation(projects.domain.card)
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.domain.transaction.error
|
||||
|
||||
sealed class SendTransactionError {
|
||||
|
||||
object DemoCardError : SendTransactionError()
|
||||
|
||||
data class DataError(val message: String?) : SendTransactionError()
|
||||
|
||||
data class NetworkError(val message: String?) : SendTransactionError()
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.tangem.domain.transaction.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
|
||||
class SendTransactionUseCase(
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
txData: TransactionData,
|
||||
userWallet: UserWallet,
|
||||
network: Network,
|
||||
): Either<SendTransactionError?, Boolean> {
|
||||
val signer = cardSdkConfigRepository.getCommonSigner(
|
||||
userWallet.cardId,
|
||||
)
|
||||
|
||||
val linkedTerminal = cardSdkConfigRepository.isLinkedTerminal()
|
||||
if (userWallet.scanResponse.card.isStart2Coin) {
|
||||
cardSdkConfigRepository.setLinkedTerminal(false)
|
||||
}
|
||||
val sendResult = try {
|
||||
if (isDemoCardUseCase(cardId = userWallet.cardId)) {
|
||||
SendTransactionError.DemoCardError.left()
|
||||
} else {
|
||||
walletManagersFacade.sendTransaction(
|
||||
txData = txData,
|
||||
signer = signer,
|
||||
userWalletId = userWallet.walletId,
|
||||
network = network,
|
||||
).right()
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
cardSdkConfigRepository.setLinkedTerminal(linkedTerminal)
|
||||
SendTransactionError.DataError(ex.message).left()
|
||||
}
|
||||
|
||||
cardSdkConfigRepository.setLinkedTerminal(linkedTerminal)
|
||||
return sendResult.fold(
|
||||
ifRight = { result ->
|
||||
when (result) {
|
||||
is SimpleResult.Success -> true.right()
|
||||
is SimpleResult.Failure -> SendTransactionError.NetworkError(result.error.message).left()
|
||||
}
|
||||
},
|
||||
ifLeft = { it.left() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,13 @@ package com.tangem.domain.wallets.usecase
|
|||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import arrow.core.right
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.wallets.legacy.WalletsStateHolder
|
||||
import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull
|
||||
import com.tangem.domain.wallets.models.SelectWalletError
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
/**
|
||||
|
|
@ -22,16 +23,20 @@ class SelectWalletUseCase(
|
|||
private val reduxStateHolder: ReduxStateHolder,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId): Either<SelectWalletError, Unit> {
|
||||
suspend operator fun invoke(userWalletId: UserWalletId): Either<SelectWalletError, UserWallet> {
|
||||
return either {
|
||||
val userWalletsListManager = ensureUserWalletListManagerNotNull(
|
||||
walletsStateHolder = walletsStateHolder,
|
||||
raise = { SelectWalletError.DataError },
|
||||
)
|
||||
|
||||
userWalletsListManager.select(userWalletId)
|
||||
.doOnFailure { raise(SelectWalletError.UnableToSelectUserWallet) }
|
||||
.doOnSuccess { reduxStateHolder.onUserWalletSelected(it) }
|
||||
return when (val result = userWalletsListManager.select(userWalletId)) {
|
||||
is CompletionResult.Failure -> raise(SelectWalletError.UnableToSelectUserWallet)
|
||||
is CompletionResult.Success -> {
|
||||
reduxStateHolder.onUserWalletSelected(result.data)
|
||||
result.data.right()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,31 @@
|
|||
package com.tangem.managetokens.presentation.common.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal sealed class ChooseWalletState {
|
||||
data class Choose(
|
||||
val wallets: ImmutableList<WalletState>,
|
||||
val selectedWallet: WalletState?,
|
||||
val onChooseWalletClick: () -> Unit,
|
||||
val onCloseChoosingWalletClick: () -> Unit,
|
||||
) : ChooseWalletState()
|
||||
|
||||
object NoSelection : ChooseWalletState()
|
||||
|
||||
class Warning(val type: ChooseWalletWarning) : ChooseWalletState() {
|
||||
val message: TextReference
|
||||
get() = when (type) {
|
||||
ChooseWalletWarning.SINGLE_CURRENCY ->
|
||||
TextReference.Res(R.string.manage_tokens_wallet_support_only_one_network_title)
|
||||
ChooseWalletWarning.WALLET_INCOMPATIBLE ->
|
||||
TextReference.Res(R.string.manage_tokens_wallet_does_not_supported_blockchain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class ChooseWalletWarning {
|
||||
SINGLE_CURRENCY,
|
||||
WALLET_INCOMPATIBLE,
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package com.tangem.managetokens.presentation.common.state
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.core.ui.extensions.getGreyedOutIconRes
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
|
||||
/**
|
||||
* Network item state
|
||||
*
|
||||
* @property name network name
|
||||
* @property protocolName network protocol name
|
||||
* @property id network id
|
||||
* @property blockchain blockchain
|
||||
* @property iconRes network icon id from resources
|
||||
*/
|
||||
internal sealed interface NetworkItemState {
|
||||
|
||||
val name: String
|
||||
val protocolName: String
|
||||
val id: String
|
||||
val blockchain: Blockchain
|
||||
|
||||
val iconRes: Int
|
||||
get() = when (this) {
|
||||
is Selectable -> this.iconResId
|
||||
is Toggleable -> this.iconResId.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Network item state that can be added and deleted
|
||||
*
|
||||
* @property name network name
|
||||
* @property protocolName network protocol name
|
||||
* @property id network id
|
||||
* @property blockchain blockchain
|
||||
* @property iconResId network icon id from resources
|
||||
* @property isMainNetwork flag that determines if the network is the main network for the token
|
||||
* @property isAdded flag that determines if the user has saved the network
|
||||
* @property address contract address
|
||||
* @property decimals decimal count
|
||||
* @property onToggleClick lambda be invoked when switch is been toggled
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class Toggleable(
|
||||
override val name: String,
|
||||
override val protocolName: String,
|
||||
override val id: String,
|
||||
override val blockchain: Blockchain,
|
||||
val iconResId: MutableState<Int>,
|
||||
val isMainNetwork: Boolean,
|
||||
val isAdded: MutableState<Boolean>,
|
||||
val address: String?,
|
||||
val decimals: Int?,
|
||||
val onToggleClick: (TokenItemState.Loaded, Toggleable) -> Unit,
|
||||
) : NetworkItemState {
|
||||
|
||||
/**
|
||||
* Change toggle state [isAdded].
|
||||
*
|
||||
* It is a hack that helps us to change element of flow
|
||||
*/
|
||||
fun changeToggleState() {
|
||||
val reverseState = !isAdded.value
|
||||
isAdded.value = reverseState
|
||||
iconResId.value = if (reverseState) getActiveIconRes(blockchain.id) else getGreyedOutIconRes(blockchain.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Network item state that can be selected
|
||||
*
|
||||
* @property name network name
|
||||
* @property protocolName network protocol name
|
||||
* @property iconResId network icon id from resources
|
||||
* @property id network id
|
||||
* @property blockchain blockchain
|
||||
* @property onNetworkClick lambda be invoked when network item is been clicked
|
||||
*
|
||||
*/
|
||||
class Selectable(
|
||||
override val name: String,
|
||||
override val protocolName: String,
|
||||
val iconResId: Int,
|
||||
override val id: String,
|
||||
override val blockchain: Blockchain,
|
||||
val onNetworkClick: (NetworkItemState) -> Unit,
|
||||
) : NetworkItemState
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.managetokens.presentation.common.state
|
||||
|
||||
internal data class WalletState(
|
||||
val walletId: String,
|
||||
val artworkUrl: String?,
|
||||
val walletName: String,
|
||||
val onSelected: (String) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.managetokens.presentation.common.state.previewdata
|
||||
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.common.state.WalletState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal object ChooseWalletStatePreviewData {
|
||||
|
||||
val state: ChooseWalletState.Choose
|
||||
get() = ChooseWalletState.Choose(
|
||||
wallets = persistentListOf(
|
||||
walletState,
|
||||
walletState.copy(walletId = "2"),
|
||||
),
|
||||
selectedWallet = walletState,
|
||||
onChooseWalletClick = {},
|
||||
onCloseChoosingWalletClick = {},
|
||||
)
|
||||
|
||||
private val walletState: WalletState
|
||||
get() = WalletState(
|
||||
walletName = "My wallet",
|
||||
walletId = "1",
|
||||
artworkUrl = "",
|
||||
onSelected = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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,151 @@
|
|||
package com.tangem.managetokens.presentation.common.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.common.state.WalletState
|
||||
import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData
|
||||
|
||||
@Composable
|
||||
internal fun ChooseWalletScreen(state: ChooseWalletState.Choose, modifier: Modifier = Modifier) {
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size44),
|
||||
) {
|
||||
IconButton(
|
||||
onClick = state.onCloseChoosingWalletClick,
|
||||
modifier = Modifier.align(Alignment.CenterStart),
|
||||
) {
|
||||
Icon(
|
||||
painterResource(id = R.drawable.ic_back_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_wallet_selector_title),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.Center),
|
||||
)
|
||||
}
|
||||
}
|
||||
items(
|
||||
count = state.wallets.count(),
|
||||
key = { index -> state.wallets[index].walletId },
|
||||
) { index ->
|
||||
WalletItem(
|
||||
wallet = state.wallets[index],
|
||||
selectedWallet = state.selectedWallet,
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = state.wallets.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing16)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WalletItem(wallet: WalletState, selectedWallet: WalletState?, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clickable { wallet.onSelected(wallet.walletId) }
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size72)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier.size(height = TangemTheme.dimens.size30, width = TangemTheme.dimens.size50),
|
||||
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(wallet.artworkUrl)
|
||||
.crossfade(enable = true)
|
||||
.build(),
|
||||
loading = {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.card_placeholder_primary),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
error = {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.card_placeholder_primary),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
SpacerW12()
|
||||
Text(
|
||||
text = wallet.walletName,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerWMax()
|
||||
if (selectedWallet == wallet) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_check_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ChooseWalletScreen_Light() {
|
||||
TangemTheme(isDark = false) {
|
||||
ChooseWalletScreen(
|
||||
state = ChooseWalletStatePreviewData.state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ChooseWalletScreen_Dark() {
|
||||
TangemTheme(isDark = false) {
|
||||
ChooseWalletScreen(
|
||||
state = ChooseWalletStatePreviewData.state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
package com.tangem.managetokens.presentation.common.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.TangemSwitch
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData
|
||||
|
||||
@Composable
|
||||
internal fun NetworkItem(state: NetworkItemState, tokenState: TokenItemState.Loaded?, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size68)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
NetworkIcon(model = state)
|
||||
SpacerW(width = TangemTheme.dimens.spacing12)
|
||||
Text(
|
||||
text = state.name,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
SpacerW(width = TangemTheme.dimens.spacing6)
|
||||
Text(
|
||||
text = state.protocolName,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
)
|
||||
if (state is NetworkItemState.Toggleable) {
|
||||
TangemSwitch(
|
||||
onCheckedChange = {
|
||||
state.onToggleClick(tokenState!!, state)
|
||||
},
|
||||
checked = state.isAdded.value,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun NetworkIcon(model: NetworkItemState, modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier.size(size = TangemTheme.dimens.size36)) {
|
||||
val isAdded = when (model) {
|
||||
is NetworkItemState.Selectable -> true
|
||||
is NetworkItemState.Toggleable -> model.isAdded.value
|
||||
}
|
||||
|
||||
if (!isAdded) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size36)
|
||||
.clip(CircleShape)
|
||||
.background(TangemTheme.colors.control.unchecked),
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
painter = painterResource(id = model.iconRes),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(size = TangemTheme.dimens.size36),
|
||||
tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
||||
if (model is NetworkItemState.Toggleable && model.isMainNetwork) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.size(TangemTheme.dimens.size10)
|
||||
.clip(CircleShape)
|
||||
.background(TangemTheme.colors.stroke.transparency),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size8)
|
||||
.clip(CircleShape)
|
||||
.background(TangemTheme.colors.icon.accent),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_NetworkItem_Light(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) {
|
||||
TangemTheme(isDark = false) {
|
||||
NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_NetworkItem_Dark(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) {
|
||||
TangemTheme(isDark = true) {
|
||||
NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded)
|
||||
}
|
||||
}
|
||||
|
||||
private class NetworkItemStateProvider : CollectionPreviewParameterProvider<NetworkItemState>(
|
||||
collection = listOf(
|
||||
NetworkItemState.Toggleable(
|
||||
name = "Ethereum",
|
||||
protocolName = "ETH",
|
||||
iconResId = mutableStateOf(R.drawable.img_polygon_22),
|
||||
isMainNetwork = true,
|
||||
isAdded = mutableStateOf(true),
|
||||
id = "",
|
||||
address = "",
|
||||
onToggleClick = { _, _ -> },
|
||||
blockchain = Blockchain.Ethereum,
|
||||
decimals = 0,
|
||||
),
|
||||
NetworkItemState.Toggleable(
|
||||
name = "BNB SMART CHAIN",
|
||||
protocolName = "BEP20",
|
||||
iconResId = mutableStateOf(R.drawable.ic_bsc_16),
|
||||
isMainNetwork = false,
|
||||
isAdded = mutableStateOf(false),
|
||||
id = "",
|
||||
address = "",
|
||||
onToggleClick = { _, _ -> },
|
||||
blockchain = Blockchain.BSC,
|
||||
decimals = 0,
|
||||
),
|
||||
NetworkItemState.Selectable(
|
||||
name = "Ethereum",
|
||||
protocolName = "ETH",
|
||||
iconResId = R.drawable.img_polygon_22,
|
||||
id = "",
|
||||
onNetworkClick = { },
|
||||
blockchain = Blockchain.Ethereum,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.managetokens.presentation.common.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun SimpleSelectionBlock(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
roundedCorners: Boolean = true,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.then(
|
||||
if (roundedCorners) {
|
||||
Modifier.clip(shape = RoundedCornerShape(TangemTheme.dimens.radius16))
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.background(color = TangemTheme.colors.background.action)
|
||||
.clickable { onClick() }
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing20,
|
||||
vertical = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
SpacerH(height = TangemTheme.dimens.spacing4)
|
||||
Text(
|
||||
text = subtitle,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_SimpleSelectionBlock_Light() {
|
||||
TangemTheme(isDark = false) {
|
||||
SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { })
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_SimpleSelectionBlock_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state
|
||||
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class ChooseNetworkState(
|
||||
val nativeNetworks: ImmutableList<NetworkItemState>,
|
||||
val nonNativeNetworks: ImmutableList<NetworkItemState>,
|
||||
val onNonNativeNetworkHintClick: () -> Unit,
|
||||
val onCloseChooseNetworkScreen: () -> Unit,
|
||||
)
|
||||
|
|
@ -8,7 +8,8 @@ import com.tangem.features.managetokens.impl.R
|
|||
|
||||
data class DerivationNotificationState(
|
||||
val totalNeeded: Int,
|
||||
val missingAddressesCount: Int,
|
||||
val totalWallets: Int,
|
||||
val walletsToDerive: Int,
|
||||
val onGenerateClick: () -> Unit,
|
||||
) {
|
||||
val config = NotificationConfig(
|
||||
|
|
@ -25,8 +26,8 @@ data class DerivationNotificationState(
|
|||
onClick = onGenerateClick,
|
||||
additionalText = pluralReference(
|
||||
id = R.plurals.manage_tokens_number_of_wallets_android,
|
||||
count = totalNeeded,
|
||||
formatArgs = wrappedList(missingAddressesCount, totalNeeded),
|
||||
count = totalWallets,
|
||||
formatArgs = wrappedList(walletsToDerive, totalWallets),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
|
||||
internal sealed class TokenItemState {
|
||||
|
||||
abstract val id: String
|
||||
|
|
@ -9,11 +11,13 @@ internal sealed class TokenItemState {
|
|||
data class Loaded(
|
||||
override val id: String,
|
||||
val name: String,
|
||||
val currencyId: String,
|
||||
val currencySymbol: String,
|
||||
val tokenId: String,
|
||||
val tokenIcon: TokenIconState,
|
||||
val quotes: QuotesState,
|
||||
val rate: String?,
|
||||
val availableAction: TokenButtonType,
|
||||
val onButtonClick: (String) -> Unit,
|
||||
val availableAction: MutableState<TokenButtonType>,
|
||||
val chooseNetworkState: ChooseNetworkState,
|
||||
val onButtonClick: (Loaded) -> Unit,
|
||||
) : TokenItemState()
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state.previewdata
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.common.state.NetworkItemState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal object ChooseNetworkStatePreviewData {
|
||||
|
||||
val state = ChooseNetworkState(
|
||||
nativeNetworks = nativeNetworks.toImmutableList(),
|
||||
nonNativeNetworks = nonNativeNetworks.toImmutableList(),
|
||||
onNonNativeNetworkHintClick = {},
|
||||
onCloseChooseNetworkScreen = {},
|
||||
)
|
||||
}
|
||||
|
||||
internal val nativeNetworks = listOf(
|
||||
NetworkItemState.Toggleable(
|
||||
name = "Ethereum",
|
||||
protocolName = "ETH",
|
||||
iconResId = mutableStateOf(R.drawable.img_polygon_22),
|
||||
isMainNetwork = true,
|
||||
isAdded = mutableStateOf(true),
|
||||
id = "",
|
||||
onToggleClick = { _, _ -> },
|
||||
blockchain = Blockchain.Ethereum,
|
||||
address = "",
|
||||
decimals = 0,
|
||||
),
|
||||
)
|
||||
|
||||
internal val nonNativeNetworks = listOf(
|
||||
NetworkItemState.Toggleable(
|
||||
name = "Ethereum",
|
||||
protocolName = "ETH",
|
||||
iconResId = mutableStateOf(R.drawable.img_kusama_22),
|
||||
isMainNetwork = false,
|
||||
isAdded = mutableStateOf(true),
|
||||
id = "",
|
||||
onToggleClick = { _, _ -> },
|
||||
blockchain = Blockchain.Ethereum,
|
||||
address = "",
|
||||
decimals = 0,
|
||||
),
|
||||
NetworkItemState.Toggleable(
|
||||
name = "BNB SMART CHAIN",
|
||||
protocolName = "BEP20",
|
||||
iconResId = mutableStateOf(R.drawable.ic_bsc_16),
|
||||
isMainNetwork = false,
|
||||
isAdded = mutableStateOf(false),
|
||||
id = "",
|
||||
onToggleClick = { _, _ -> },
|
||||
blockchain = Blockchain.BSC,
|
||||
address = "",
|
||||
decimals = 0,
|
||||
),
|
||||
)
|
||||
|
|
@ -4,8 +4,9 @@ import com.tangem.managetokens.presentation.managetokens.state.DerivationNotific
|
|||
|
||||
object DerivationNotificationStatePreviewData {
|
||||
val state = DerivationNotificationState(
|
||||
totalNeeded = 3,
|
||||
missingAddressesCount = 2,
|
||||
totalNeeded = 5,
|
||||
totalWallets = 3,
|
||||
walletsToDerive = 2,
|
||||
onGenerateClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.state.previewdata
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.managetokens.presentation.managetokens.state.*
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -13,7 +14,8 @@ internal object TokenItemStatePreviewData {
|
|||
get() = TokenItemState.Loaded(
|
||||
id = "BTC",
|
||||
name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin",
|
||||
currencyId = "BTC",
|
||||
tokenId = "BTC",
|
||||
currencySymbol = "BTC",
|
||||
tokenIcon = tokenIconState,
|
||||
quotes = QuotesState.Content(
|
||||
priceChange = "0.43%",
|
||||
|
|
@ -21,15 +23,17 @@ internal object TokenItemStatePreviewData {
|
|||
chartData = persistentListOf(10f, 2f, 5f, 3f, 4f, 8f, 9f, 7f, 4f),
|
||||
),
|
||||
rate = "31 285.72$",
|
||||
availableAction = TokenButtonType.ADD,
|
||||
availableAction = mutableStateOf(TokenButtonType.ADD),
|
||||
onButtonClick = {},
|
||||
chooseNetworkState = ChooseNetworkStatePreviewData.state,
|
||||
)
|
||||
|
||||
val loadedPriceUp: TokenItemState
|
||||
get() = TokenItemState.Loaded(
|
||||
id = "BTC",
|
||||
name = "Bitcoin",
|
||||
currencyId = "BTC",
|
||||
tokenId = "BTC",
|
||||
currencySymbol = "BTC",
|
||||
tokenIcon = tokenIconState,
|
||||
quotes = QuotesState.Content(
|
||||
priceChange = "0.43%",
|
||||
|
|
@ -37,8 +41,9 @@ internal object TokenItemStatePreviewData {
|
|||
chartData = persistentListOf(1f, 3f, 4f, 8f, 12f, 10f, 8f, 3f, 5f, 7f),
|
||||
),
|
||||
rate = "31 285.72$",
|
||||
availableAction = TokenButtonType.NOT_AVAILABLE,
|
||||
availableAction = mutableStateOf(TokenButtonType.NOT_AVAILABLE),
|
||||
onButtonClick = {},
|
||||
chooseNetworkState = ChooseNetworkStatePreviewData.state,
|
||||
)
|
||||
|
||||
private val tokenIconState: TokenIconState
|
||||
|
|
|
|||
|
|
@ -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,205 @@
|
|||
package com.tangem.managetokens.presentation.managetokens.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.WarningCardTitleOnly
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.managetokens.impl.R
|
||||
import com.tangem.managetokens.presentation.common.state.ChooseWalletState
|
||||
import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData
|
||||
import com.tangem.managetokens.presentation.common.ui.components.NetworkItem
|
||||
import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock
|
||||
import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.TokenItemState
|
||||
import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData
|
||||
|
||||
@Composable
|
||||
internal fun ChooseNetworkScreen(
|
||||
state: TokenItemState.Loaded,
|
||||
walletState: ChooseWalletState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val networkState = state.chooseNetworkState
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_network_selector_title),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
when (walletState) {
|
||||
is ChooseWalletState.Choose -> {
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing10)
|
||||
}
|
||||
item {
|
||||
SimpleSelectionBlock(
|
||||
title = stringResource(id = R.string.manage_tokens_network_selector_wallet),
|
||||
subtitle = walletState.selectedWallet?.walletName ?: "",
|
||||
onClick = walletState.onChooseWalletClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
ChooseWalletState.NoSelection -> Unit
|
||||
is ChooseWalletState.Warning -> {
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing10)
|
||||
}
|
||||
item {
|
||||
WarningCardTitleOnly(
|
||||
title = stringResource(id = R.string.manage_tokens_wallet_support_only_one_network_title),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing16)
|
||||
}
|
||||
|
||||
item {
|
||||
if (networkState.nativeNetworks.isNotEmpty()) {
|
||||
NativeNetworks(networkState = networkState, tokenState = state)
|
||||
}
|
||||
}
|
||||
|
||||
if (networkState.nonNativeNetworks.isNotEmpty()) {
|
||||
item {
|
||||
NonNativeNetworksHeader(networkState.onNonNativeNetworkHintClick)
|
||||
}
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing8)
|
||||
}
|
||||
item {
|
||||
this@LazyColumn.NonNativeNetworks(networkState = networkState, tokenState = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_network_selector_native_title),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
)
|
||||
SpacerH(height = TangemTheme.dimens.spacing2)
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_network_selector_native_subtitle),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
SpacerH(height = TangemTheme.dimens.spacing8)
|
||||
|
||||
networkState.nativeNetworks.forEachIndexed { index, network ->
|
||||
NetworkItem(
|
||||
state = network,
|
||||
tokenState = tokenState,
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = networkState.nativeNetworks.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
SpacerH(height = TangemTheme.dimens.spacing16)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LazyListScope.NonNativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) {
|
||||
items(
|
||||
count = networkState.nonNativeNetworks.count(),
|
||||
key = { index -> networkState.nonNativeNetworks[index].id },
|
||||
) { index ->
|
||||
NetworkItem(
|
||||
state = networkState.nonNativeNetworks[index],
|
||||
tokenState = tokenState,
|
||||
modifier = Modifier
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = networkState.nonNativeNetworks.lastIndex,
|
||||
addDefaultPadding = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
item {
|
||||
SpacerH(height = TangemTheme.dimens.spacing16)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NonNativeNetworksHeader(onNonNativeNetworkHintClick: () -> Unit) {
|
||||
Column {
|
||||
Row {
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_network_selector_non_native_title),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
)
|
||||
SpacerW(width = TangemTheme.dimens.spacing2)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_information_24),
|
||||
tint = TangemTheme.colors.icon.inactive,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.clickable { onNonNativeNetworkHintClick() },
|
||||
)
|
||||
}
|
||||
SpacerH(height = TangemTheme.dimens.spacing2)
|
||||
Text(
|
||||
text = stringResource(id = R.string.manage_tokens_network_selector_non_native_subtitle),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ChooseNetworkScreen_Light() {
|
||||
TangemTheme(isDark = false) {
|
||||
ChooseNetworkScreen(
|
||||
state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded,
|
||||
walletState = ChooseWalletStatePreviewData.state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_ChooseNetworkScreen_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
ChooseNetworkScreen(
|
||||
state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded,
|
||||
walletState = ChooseWalletStatePreviewData.state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = { })
|
||||
}
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ private fun LoadedTokenItem(state: TokenItemState.Loaded, modifier: Modifier = M
|
|||
modifier = Modifier
|
||||
.weight(weight = 1f),
|
||||
) {
|
||||
TokenName(name = state.name, currencyId = state.currencyId)
|
||||
TokenName(name = state.name, currencyId = state.currencySymbol)
|
||||
TokenPriceData(price = state.rate, quotesState = state.quotes)
|
||||
}
|
||||
SpacerW24()
|
||||
|
|
@ -67,8 +67,8 @@ private fun LoadedTokenItem(state: TokenItemState.Loaded, modifier: Modifier = M
|
|||
}
|
||||
|
||||
TokenButton(
|
||||
type = state.availableAction,
|
||||
onClick = { state.onButtonClick(state.currencyId) },
|
||||
type = state.availableAction.value,
|
||||
onClick = { state.onButtonClick(state) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ dependencies {
|
|||
implementation(projects.core.featuretoggles)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.models)
|
||||
|
|
@ -58,6 +59,8 @@ dependencies {
|
|||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.demo)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.send.api)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.send.impl.di
|
||||
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.navigation.DefaultSendRouter
|
||||
import dagger.Module
|
||||
|
|
@ -17,7 +18,7 @@ internal object SendRouterModule {
|
|||
|
||||
@Provides
|
||||
@ActivityScoped
|
||||
fun provideSendRouter(): SendRouter {
|
||||
return DefaultSendRouter()
|
||||
fun provideSendRouter(reduxNavController: ReduxNavController): SendRouter {
|
||||
return DefaultSendRouter(reduxNavController)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,17 @@
|
|||
package com.tangem.features.send.impl.navigation
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.navigation.ReduxNavController
|
||||
import com.tangem.features.send.impl.presentation.SendFragment
|
||||
|
||||
internal class DefaultSendRouter : SendRouter {
|
||||
internal class DefaultSendRouter(
|
||||
private val reduxNavController: ReduxNavController,
|
||||
) : InnerSendRouter {
|
||||
|
||||
override fun getEntryFragment(): Fragment = SendFragment.create()
|
||||
|
||||
override fun openUrl(url: String) {
|
||||
reduxNavController.navigate(NavigationAction.OpenUrl(url = url))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.send.impl.navigation
|
||||
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
|
||||
interface InnerSendRouter : SendRouter {
|
||||
|
||||
/** Open website by [url] */
|
||||
fun openUrl(url: String)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue