Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-26 13:14:53 +03:00
commit 43c376489a
316 changed files with 7586 additions and 3131 deletions

View file

@ -10,7 +10,7 @@ import com.tangem.core.navigation.url.UrlOpener
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -76,7 +76,7 @@ interface ApplicationEntryPoint {
fun getBalanceHidingRepository(): BalanceHidingRepository
fun getUserTokensStore(): UserTokensStore
fun getAppPreferencesStore(): AppPreferencesStore
fun getGetAppThemeModeUseCase(): GetAppThemeModeUseCase

View file

@ -22,7 +22,7 @@ import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.datasource.config.models.Config
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -126,8 +126,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val balanceHidingRepository: BalanceHidingRepository
get() = entryPoint.getBalanceHidingRepository()
private val userTokensStore: UserTokensStore
get() = entryPoint.getUserTokensStore()
private val appPreferencesStore: AppPreferencesStore
get() = entryPoint.getAppPreferencesStore()
val getAppThemeModeUseCase: GetAppThemeModeUseCase
get() = entryPoint.getGetAppThemeModeUseCase()
@ -228,7 +228,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
}
derivationsFinder = DerivationsFinder(
newTokensStore = userTokensStore,
appPreferencesStore = appPreferencesStore,
dispatchers = AppCoroutineDispatcherProvider(),
)
appStateHolder.mainStore = store

View file

@ -1,10 +1,8 @@
package com.tangem.tap.di.domain
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
import com.tangem.domain.markets.GetTokenPriceChartUseCase
import com.tangem.domain.markets.GetTokenQuotesUseCase
import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -37,7 +35,13 @@ object MarketsDomainModule {
@Provides
@Singleton
fun provideGetTokenQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenQuotesUseCase {
return GetTokenQuotesUseCase(marketsTokenRepository = marketsTokenRepository)
fun provideTokenFullQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenFullQuotesUseCase {
return GetTokenFullQuotesUseCase(marketsTokenRepository = marketsTokenRepository)
}
@Provides
@Singleton
fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetTokenQuotesUseCase {
return GetTokenQuotesUseCase(quotesRepository = quotesRepository)
}
}

View file

@ -145,18 +145,6 @@ internal object StakingDomainModule {
)
}
@Provides
@Singleton
fun provideIsStakeMoreAvailableUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): IsStakeMoreAvailableUseCase {
return IsStakeMoreAvailableUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideIsApproveNeededUseCase(

View file

@ -158,8 +158,9 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository)
return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository)
}
@Provides

View file

@ -5,7 +5,10 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.models.scan.CardDTO
@ -22,7 +25,7 @@ internal data class BlockchainToDerive(
// FIXME: May be move to DI, currently unnecessary
internal class DerivationsFinder(
private val newTokensStore: UserTokensStore,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -64,7 +67,9 @@ internal class DerivationsFinder(
}
private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet<BlockchainToDerive> {
val responseTokens = newTokensStore.getSyncOrNull(userWalletId)
val responseTokens = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
)
?.tokens
?: return hashSetOf()

View file

@ -7,7 +7,7 @@ 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.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
@ -20,12 +20,12 @@ internal fun SettingsAlertDialog(dialog: Dialog.Alert) {
title = dialog.title.resolveReference(),
message = dialog.description.resolveReference(),
isDismissable = false,
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = dialog.confirmText.resolveReference(),
warning = true,
onClick = dialog.onConfirm,
),
dismissButton = DialogButton(
dismissButton = DialogButtonUM(
title = stringResource(id = R.string.common_cancel),
onClick = dialog.onDismiss,
),

View file

@ -6,7 +6,7 @@ import androidx.compose.ui.res.stringResource
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.core.ui.components.DialogButton
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.SelectorDialog
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
@ -21,7 +21,7 @@ internal fun SettingsSelectorDialog(dialog: Dialog.Selector) {
title = dialog.title.resolveReference(),
selectedItemIndex = dialog.selectedItemIndex,
items = dialog.items.map { it.resolveReference() }.toImmutableList(),
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(R.string.common_cancel),
onClick = dialog.onDismiss,
),

View file

@ -13,6 +13,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.getBackupCardsCount
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
@ -173,13 +174,7 @@ internal class CardSettingsViewModel @Inject constructor(
userWalletId = userWalletId,
cardId = card.cardId,
isActiveBackupStatus = card.backupStatus?.isActive == true,
backupCardsCount = when (val status = card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount
is CardDTO.BackupStatus.CardLinked,
CardDTO.BackupStatus.NoBackup,
null,
-> 0
},
backupCardsCount = scanResponse.getBackupCardsCount() ?: 0,
),
)
}

View file

@ -190,11 +190,11 @@ private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) {
BasicDialog(
title = stringResource(dialog.titleResId),
message = stringResource(dialog.messageResId),
dismissButton = DialogButton(
dismissButton = DialogButtonUM(
title = stringResource(id = R.string.common_cancel),
onClick = dialog.onDismiss,
),
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.card_settings_action_sheet_reset),
warning = true,
onClick = dialog.onConfirmClick,
@ -208,7 +208,7 @@ private fun CompletedResetDialog(dialog: ResetCardDialog) {
BasicDialog(
title = stringResource(id = dialog.titleResId),
message = stringResource(id = dialog.messageResId),
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
onClick = dialog.onConfirmClick,
),

View file

@ -98,7 +98,7 @@ internal class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
viewModel.onSearchClick()
} else {
Analytics.send(IntroductionProcess.ButtonTokensList())
store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) }
store.dispatchNavigationAction { push(AppRoute.ManageTokens()) }
store.dispatch(TokensAction.SetArgs.ReadAccess)
}
},

View file

@ -61,7 +61,7 @@ internal class HomeViewModel @Inject constructor(
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList())
store.dispatch(TokensAction.SetArgs.ReadAccess)
store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) }
store.dispatchNavigationAction { push(AppRoute.ManageTokens()) }
}
private fun scanCard() {

View file

@ -9,9 +9,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
@Composable
@ -19,11 +19,11 @@ fun EnrollBiometricsDialogContent(dialog: EnrollBiometricsDialog) {
BasicDialog(
title = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_title),
message = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_description),
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(R.string.common_enable),
onClick = dialog.onEnroll,
),
dismissButton = DialogButton(
dismissButton = DialogButtonUM(
onClick = dialog.onCancel,
),
onDismissDialog = dialog.onCancel,

View file

@ -7,7 +7,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.welcome.ui.model.WarningModel
import com.tangem.wallet.R
@ -27,7 +27,7 @@ internal fun WarningDialog(warning: WarningModel?) {
},
),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),
@ -38,7 +38,7 @@ internal fun WarningDialog(warning: WarningModel?) {
title = stringResource(id = R.string.common_attention),
message = stringResource(id = R.string.key_invalidated_warning_description),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),
@ -50,7 +50,7 @@ internal fun WarningDialog(warning: WarningModel?) {
message = stringResource(id = R.string.biometric_unavailable_warning),
onDismissDialog = warning.onDismiss,
isDismissable = false,
confirmButton = DialogButton(
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),

View file

@ -11,6 +11,7 @@ import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.managetokens.ManageTokensToggles
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.send.api.navigation.SendRouter
@ -50,6 +51,7 @@ internal class ChildFactory @Inject constructor(
private val walletSettingsComponentFactory: WalletSettingsComponent.Factory,
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
private val sendRouter: SendRouter,
private val tokenDetailsRouter: TokenDetailsRouter,
private val walletRouter: WalletRouter,
@ -126,13 +128,7 @@ internal class ChildFactory @Inject constructor(
if (manageTokensToggles.isFeatureEnabled) {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = ManageTokensComponent.Params(
mode = if (route.readOnlyContent) {
ManageTokensComponent.Mode.READ_ONLY
} else {
ManageTokensComponent.Mode.MANAGE
},
),
params = ManageTokensComponent.Params(route.userWalletId),
componentFactory = manageTokensComponentFactory,
)
} else {
@ -191,6 +187,16 @@ internal class ChildFactory @Inject constructor(
componentFactory = walletSettingsComponentFactory,
)
}
is AppRoute.MarketsTokenDetails -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = MarketsTokenDetailsComponent.Params(
token = route.token,
appCurrency = route.appCurrency,
),
componentFactory = marketsTokenDetailsComponentFactory,
)
}
}
}

View file

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/llTotalContainer"
android:layout_width="match_parent"
android:layout_height="64dp"
android:gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:id="@+id/llTotal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvTotal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/send_total_label"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvTotalValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textAllCaps="true"
android:textSize="20sp"
android:textStyle="bold"
tools:text="usd" />
</FrameLayout>
<TextView
android:id="@+id/tvWillBeSentValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="4dp"
android:gravity="end"
android:textColor="@color/text_tertiary"
tools:text="123.29837729 ADA 39487593.109342039402938049 will be sent" />
</LinearLayout>
<FrameLayout
android:id="@+id/flTotalTokenCrypto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<TextView
android:id="@+id/tvTotalTokenCrypto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:text="@string/send_total_label"
android:textColor="@color/text_tertiary"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvTotalTokenCryptoValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textAllCaps="true"
android:textColor="@color/text_tertiary"
android:textSize="14sp"
android:textStyle="bold"
tools:text="usd" />
</FrameLayout>
</LinearLayout>

View file

@ -19,6 +19,8 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.markets.models)
implementation(projects.domain.appCurrency.models)
/* Libs - Other */
api(deps.kotlin.serialization)

View file

@ -5,6 +5,8 @@ import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrency
@ -177,8 +179,8 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class ManageTokens(
val readOnlyContent: Boolean,
) : AppRoute(path = "/manage_tokens/$readOnlyContent"), RouteBundleParams {
val userWalletId: UserWalletId? = null,
) : AppRoute(path = "/manage_tokens/$userWalletId"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}
@ -262,4 +264,10 @@ sealed class AppRoute(val path: String) : Route {
data class WalletSettings(
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_settings/${userWalletId.stringValue}")
@Serializable
data class MarketsTokenDetails(
val token: TokenMarketParams,
val appCurrency: AppCurrency,
) : AppRoute(path = "/markets_token_details/${token.id}")
}

View file

@ -0,0 +1,51 @@
package com.tangem.common.routing.utils
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.decompose.navigation.Router
import kotlin.reflect.KClass
/**
* Temporary solution to convert [AppRouter] to [Router].
* (through manual ComponentContext creation).
*
* **Will be removed when all screens will be migrated to Decompose.**
*
* @return [Router] that wraps [AppRouter].
*/
fun AppRouter.asRouter(): Router {
return RouterProxy(appRouter = this)
}
private class RouterProxy(
private val appRouter: AppRouter,
) : Router {
override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
if (route is AppRoute) {
appRouter.push(route, onComplete)
}
}
override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) {
routes.filterIsInstance<AppRoute>().let {
appRouter.replaceAll(*it.toTypedArray(), onComplete = onComplete)
}
}
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
appRouter.pop(onComplete)
}
override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
if (route is AppRoute) {
appRouter.popTo(route, onComplete)
}
}
@Suppress("UNCHECKED_CAST")
override fun popTo(routeClass: KClass<out Route>, onComplete: (isSuccess: Boolean) -> Unit) {
appRouter.popTo(routeClass as KClass<out AppRoute>, onComplete)
}
}

View file

@ -22,6 +22,11 @@ class PriceAndTimePointValuesConverter(
private val formatYValuesCache = mutableMapOf<Double, BigDecimal>()
private val formatXValuesCache = mutableMapOf<Double, BigDecimal>()
private data class Point(
val x: BigDecimal,
val y: BigDecimal,
)
override fun convert(data: MarketChartData.Data): MarketChartRawData {
formatYValuesCache.clear()
formatXValuesCache.clear()
@ -33,8 +38,10 @@ class PriceAndTimePointValuesConverter(
)
minMaxCache = cache
val normY = data.y.normalizeToDouble(min = cache.minY, max = cache.maxY)
val normX = data.x.normalizeTime(min = cache.minX, max = cache.maxX)
val points = data.x.zip(data.y) { x, y -> Point(x, y) }.sortedBy { it.x }
val normY = points.map { it.y }.normalizeToDouble(min = cache.minY, max = cache.maxY)
val normX = points.map { it.x }.normalizeTime(min = cache.minX, max = cache.maxX)
return if (normX.size > MAX_POINTS) {
LTThreeBuckets

View file

@ -18,6 +18,7 @@ dependencies {
implementation(deps.compose.ui.tooling)
implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt)
implementation(deps.compose.coil)
/** Deps */
implementation(deps.kotlin.immutable.collections)
@ -30,6 +31,8 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.transaction.models)
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}

View file

@ -0,0 +1,49 @@
package com.tangem.common.ui.alerts
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
import com.tangem.common.ui.alerts.models.AlertTransactionErrorUM
import com.tangem.common.ui.alerts.models.AlertUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.utils.converter.Converter
class SendTransactionAlertConverter(
private val popBackStack: () -> Unit,
private val onFailedTxEmailClick: (String) -> Unit,
) : Converter<SendTransactionError, AlertUM?> {
override fun convert(value: SendTransactionError): AlertUM? {
return when (value) {
is SendTransactionError.DemoCardError -> AlertDemoModeUM(
onConfirmClick = popBackStack,
)
is SendTransactionError.TangemSdkError -> AlertTransactionErrorUM(
code = value.code.toString(),
cause = null,
causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)),
onConfirmClick = { onFailedTxEmailClick(value.code.toString()) },
)
is SendTransactionError.BlockchainSdkError -> AlertTransactionErrorUM(
code = value.code.toString(),
cause = value.message,
onConfirmClick = { onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") },
)
is SendTransactionError.DataError -> AlertTransactionErrorUM(
code = "",
cause = value.message,
onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) },
)
is SendTransactionError.NetworkError -> AlertTransactionErrorUM(
code = value.code.orEmpty(),
cause = value.message.orEmpty(),
onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) },
)
is SendTransactionError.UnknownError -> AlertTransactionErrorUM(
code = "",
cause = value.ex?.localizedMessage,
onConfirmClick = { onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) },
)
else -> null
}
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.common.ui.alerts.models
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
data class AlertDemoModeUM(
override val onConfirmClick: () -> Unit,
) : AlertUM {
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title)
override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message)
}

View file

@ -0,0 +1,21 @@
package com.tangem.common.ui.alerts.models
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
data class AlertTransactionErrorUM(
val code: String,
val cause: String?,
val causeTextReference: TextReference? = null,
override val onConfirmClick: (() -> Unit)? = null,
) : AlertUM {
override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title)
override val message: TextReference = resourceReference(
id = R.string.send_alert_transaction_failed_text,
formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code),
)
override val confirmButtonText: TextReference =
resourceReference(id = R.string.common_support)
}

View file

@ -0,0 +1,12 @@
package com.tangem.common.ui.alerts.models
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
interface AlertUM {
val title: TextReference?
val message: TextReference
val confirmButtonText: TextReference
val onConfirmClick: (() -> Unit)?
}

View file

@ -1,6 +1,7 @@
package com.tangem.common.ui.amountScreen.converters.field
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountState
@ -81,6 +82,10 @@ class AmountFieldChangeTransformer(
cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO),
fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO),
isError = false,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.None,
keyboardType = KeyboardType.Number,
),
),
)
}

View file

@ -59,7 +59,7 @@ fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) {
BasicDialog(
message = content.data.dialogText.resolveReference(),
title = stringResource(id = R.string.common_approve),
confirmButton = DialogButton { isPermissionAlertShow = false },
confirmButton = DialogButtonUM { isPermissionAlertShow = false },
onDismissDialog = {},
)
}

View file

@ -7,8 +7,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -51,18 +50,10 @@ fun NavigationButtonsBlock(buttonState: NavigationButtonsState, modifier: Modifi
@Composable
private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) {
val wrappedButton by rememberNavigationButton(primaryButton)
AnimatedContent(
targetState = primaryButton,
transitionSpec = {
val isPrimaryToHide = targetState != null && initialState == null
val isPrimaryWasVisible = targetState == null && initialState != null
if (isPrimaryToHide || isPrimaryWasVisible) {
slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn())
.togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()))
} else {
fadeIn().togetherWith(fadeOut())
}
},
targetState = wrappedButton,
transitionSpec = { navigationButtonsTransition() },
contentAlignment = Alignment.Center,
label = "Animate show primary button",
modifier = modifier.fillMaxWidth(),
@ -90,18 +81,10 @@ private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier =
@Composable
private fun SecondaryButton(secondaryButton: NavigationButton?) {
val wrappedButton by rememberNavigationButton(secondaryButton)
AnimatedContent(
targetState = secondaryButton,
transitionSpec = {
val isPrimaryToHide = targetState != null && initialState == null
val isPrimaryWasVisible = targetState == null && initialState != null
if (isPrimaryToHide || isPrimaryWasVisible) {
slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn())
.togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()))
} else {
fadeIn().togetherWith(fadeOut())
}
},
targetState = wrappedButton,
transitionSpec = { navigationButtonsTransition() },
contentAlignment = Alignment.Center,
label = "Animate show secondary button",
modifier = Modifier.fillMaxWidth(),
@ -182,6 +165,28 @@ private fun ExtraButtons(extraButtons: ImmutableList<NavigationButton>?, txUrl:
}
}
@Composable
private fun rememberNavigationButton(button: NavigationButton?): MutableState<NavigationButton?> {
return remember(
button?.iconRes,
button?.isIconVisible,
button?.isEnabled,
button?.showProgress,
button?.textReference,
) { mutableStateOf(button) }
}
private fun <T> AnimatedContentTransitionScope<T>.navigationButtonsTransition(): ContentTransform {
val isPrimaryToHide = targetState != null && initialState == null
val isPrimaryWasVisible = targetState == null && initialState != null
return if (isPrimaryToHide || isPrimaryWasVisible) {
slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn())
.togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()))
} else {
fadeIn().togetherWith(fadeOut())
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -0,0 +1,213 @@
package com.tangem.common.ui.userwallet
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CardColors
import androidx.compose.material3.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.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEach
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.common.ui.R
import com.tangem.core.ui.coil.RotationTransformation
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.collections.immutable.persistentListOf
@Composable
fun UserWalletItem(
state: UserWalletItemUM,
modifier: Modifier = Modifier,
blockColors: CardColors = TangemBlockCardColors,
) {
BlockCard(
modifier = modifier,
colors = blockColors,
onClick = state.onClick,
enabled = state.isEnabled,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size68)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
CardImage(imageUrl = state.imageUrl)
NameAndInfo(
modifier = Modifier.weight(1f),
name = state.name,
information = state.information,
)
when (state.endIcon) {
UserWalletItemUM.EndIcon.None -> {}
UserWalletItemUM.EndIcon.Arrow -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
UserWalletItemUM.EndIcon.Checkmark -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_check_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
}
}
}
}
}
@Composable
private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) {
Column(
modifier = modifier.heightIn(min = TangemTheme.dimens.size40),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.SpaceEvenly,
) {
Text(
text = name.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
AnimatedContent(
targetState = information.resolveReference(),
label = "User wallet information",
) { information ->
Text(
text = information,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
private fun CardImage(imageUrl: String, modifier: Modifier = Modifier) {
val imageModifier = modifier
.width(TangemTheme.dimens.size24)
.height(TangemTheme.dimens.size36)
.clip(TangemTheme.shapes.roundedCornersSmall)
SubcomposeAsyncImage(
modifier = imageModifier,
model = ImageRequest.Builder(LocalContext.current)
.transformations(RotationTransformation(angle = 90f))
.size(
width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() },
height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() },
)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = {
RectangleShimmer(
modifier = imageModifier,
radius = TangemTheme.dimens.size2,
)
},
error = {
Image(
modifier = imageModifier,
imageVector = ImageVector.vectorResource(R.drawable.img_card_wallet_2_gray_22_36),
contentDescription = null,
)
},
contentDescription = null,
)
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
val list = persistentListOf(
UserWalletItemUM(
id = UserWalletId("user_wallet_1".encodeToByteArray()),
name = stringReference("My Wallet"),
information = getInformation(3, "4 496,75 $"),
imageUrl = "",
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_2".encodeToByteArray()),
name = stringReference("Old wallet"),
information = getInformation(3, "4 496,75 $"),
imageUrl = "",
isEnabled = true,
onClick = {},
endIcon = UserWalletItemUM.EndIcon.Arrow,
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = getInformation(3, "4 496,75 $"),
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
),
)
Column(
Modifier
.background(TangemTheme.colors.background.tertiary)
.padding(TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
list.fastForEach { userWalletItemUM ->
UserWalletItem(
modifier = Modifier.fillMaxWidth(),
state = userWalletItemUM,
)
}
}
}
}
private fun getInformation(cardCount: Int, totalBalance: String): TextReference {
val t1 = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
val divider = stringReference(value = "")
val t2 = stringReference(totalBalance)
return TextReference.Combined(wrappedList(t1, divider, t2))
}

View file

@ -0,0 +1,22 @@
package com.tangem.common.ui.userwallet.state
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.wallets.models.UserWalletId
import javax.annotation.concurrent.Immutable
@Immutable
data class UserWalletItemUM(
val id: UserWalletId,
val name: TextReference,
val information: TextReference,
val imageUrl: String,
val isEnabled: Boolean,
val endIcon: EndIcon = EndIcon.None,
val onClick: () -> Unit,
) {
enum class EndIcon {
None,
Arrow,
Checkmark,
}
}

View file

@ -34,7 +34,7 @@ interface StakeKitApi {
@POST("yields/balances")
suspend fun getMultipleYieldBalances(
@Body body: List<YieldBalanceRequestBody>,
): ApiResponse<List<YieldBalanceWrapperDTO>>
): ApiResponse<Set<YieldBalanceWrapperDTO>>
@POST("yields/{integrationId}/balances")
suspend fun getSingleYieldBalance(

View file

@ -48,7 +48,7 @@ data class ActionRequestBodyArgs(
@Json(name = "ledgerWalletAPICompatible")
val ledgerWalletAPICompatible: Boolean? = null,
@Json(name = "tronResource")
val tronResource: String? = null,
val tronResource: TronResource? = null,
@Json(name = "signatureVerification")
val signatureVerification: SignatureVerification? = null,
@Json(name = "inputToken")
@ -60,4 +60,12 @@ data class SignatureVerification(
val message: String,
@Json(name = "signed")
val signed: String,
)
)
enum class TronResource {
@Json(name = "ENERGY")
ENERGY,
@Json(name = "BANDWIDTH")
BANDWIDTH,
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.stakekit.models.response.model.transaction.tron
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class TronStakeKitTransaction(
@Json(name = "raw_data_hex")
val rawDataHex: String,
)

View file

@ -1,34 +0,0 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.token.AppPreferencesUserTokensStore
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.token.UserTokensStoreMigrationRunner
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object UserTokensStoreModule {
@Provides
@Singleton
fun provideUserTokensStore(
appPreferencesStore: AppPreferencesStore,
userTokensStoreMigrationRunner: UserTokensStoreMigrationRunner,
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
): UserTokensStore {
return AppPreferencesUserTokensStore(
appPreferencesStore = appPreferencesStore,
userTokensStoreMigrationRunner = userTokensStoreMigrationRunner,
userWalletsStore = userWalletsStore,
dispatchers = dispatchers,
)
}
}

View file

@ -1,63 +0,0 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
/**
* Implementation of [UserTokensStore] that based on [appPreferencesStore]
*
* @property appPreferencesStore application preference store
*
[REDACTED_AUTHOR]
*/
internal class AppPreferencesUserTokensStore(
private val appPreferencesStore: AppPreferencesStore,
private val userTokensStoreMigrationRunner: UserTokensStoreMigrationRunner,
private val userWalletsStore: UserWalletsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : UserTokensStore {
init {
runUserTokensMigrations()
}
override fun get(key: UserWalletId): Flow<UserTokensResponse> {
return appPreferencesStore
.getObject<UserTokensResponse>(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue))
.filterNotNull()
}
override suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse? {
return appPreferencesStore.getObjectSyncOrNull(
key = PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue),
)
}
override suspend fun store(key: UserWalletId, value: UserTokensResponse) {
appPreferencesStore.storeObject(
key = PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue),
value = value,
)
}
// TODO: delete in 5.15 (Mobile Sprint 161) [REDACTED_JIRA]
private fun runUserTokensMigrations() {
userWalletsStore.userWallets
.filter { it.isNotEmpty() }
.take(1)
.onEach { userWallets ->
userTokensStoreMigrationRunner.run(ids = userWallets.map { it.walletId.stringValue })
}
.flowOn(dispatchers.io)
.launchIn(CoroutineScope(dispatchers.io))
}
}

View file

@ -3,49 +3,53 @@ package com.tangem.datasource.local.token
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
internal class DefaultStakingBalanceStore(
private val dataStore: StringKeyDataStore<List<YieldBalanceWrapperDTO>>,
private val dataStore: StringKeyDataStore<Set<YieldBalanceWrapperDTO>>,
) : StakingBalanceStore {
override fun get(): Flow<List<YieldBalanceWrapperDTO>> {
return dataStore.get(STAKING_BALANCE_KEY)
private val mutex = Mutex()
override fun get(userWalletId: UserWalletId): Flow<Set<YieldBalanceWrapperDTO>> {
return dataStore.get(userWalletId.stringValue)
}
override suspend fun getSyncOrNull(): List<YieldBalanceWrapperDTO>? {
return dataStore.getSyncOrNull(STAKING_BALANCE_KEY)
override suspend fun getSyncOrNull(userWalletId: UserWalletId): Set<YieldBalanceWrapperDTO>? {
return dataStore.getSyncOrNull(userWalletId.stringValue)
}
override suspend fun store(items: List<YieldBalanceWrapperDTO>) {
return dataStore.store(STAKING_BALANCE_KEY, items)
override suspend fun store(userWalletId: UserWalletId, items: Set<YieldBalanceWrapperDTO>) {
mutex.withLock {
dataStore.store(userWalletId.stringValue, items)
}
}
override fun get(integrationId: String): Flow<List<BalanceDTO>> {
return dataStore.get(STAKING_BALANCE_KEY)
override fun get(userWalletId: UserWalletId, integrationId: String): Flow<List<BalanceDTO>> {
return dataStore.get(userWalletId.stringValue)
.map { balances ->
balances.filter { it.integrationId == integrationId }
.flatMap { it.balances }
}
}
override suspend fun getSyncOrNull(integrationId: String): List<BalanceDTO>? {
return dataStore.getSyncOrNull(STAKING_BALANCE_KEY)
override suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): List<BalanceDTO>? {
return dataStore.getSyncOrNull(userWalletId.stringValue)
?.firstOrNull { it.integrationId == integrationId }?.balances
}
override suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO) {
val balances = dataStore.getSyncOrNull(STAKING_BALANCE_KEY)
?.toMutableList()
?.addOrReplace(item) { item.integrationId == integrationId }
?: listOf(item)
override suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO) {
mutex.withLock {
val balances = dataStore.getSyncOrNull(userWalletId.stringValue)
?.addOrReplace(item) { it.integrationId == integrationId }
?: setOf(item)
return dataStore.store(STAKING_BALANCE_KEY, balances)
}
companion object {
private const val STAKING_BALANCE_KEY = "STAKING_BALANCE_KEY"
dataStore.store(userWalletId.stringValue, balances)
}
}
}

View file

@ -2,19 +2,20 @@ package com.tangem.datasource.local.token
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
interface StakingBalanceStore {
fun get(): Flow<List<YieldBalanceWrapperDTO>>
fun get(userWalletId: UserWalletId): Flow<Set<YieldBalanceWrapperDTO>>
suspend fun getSyncOrNull(): List<YieldBalanceWrapperDTO>?
suspend fun getSyncOrNull(userWalletId: UserWalletId): Set<YieldBalanceWrapperDTO>?
suspend fun store(items: List<YieldBalanceWrapperDTO>)
suspend fun store(userWalletId: UserWalletId, items: Set<YieldBalanceWrapperDTO>)
fun get(integrationId: String): Flow<List<BalanceDTO>>
fun get(userWalletId: UserWalletId, integrationId: String): Flow<List<BalanceDTO>>
suspend fun getSyncOrNull(integrationId: String): List<BalanceDTO>?
suspend fun getSyncOrNull(userWalletId: UserWalletId, integrationId: String): List<BalanceDTO>?
suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO)
suspend fun store(userWalletId: UserWalletId, integrationId: String, item: YieldBalanceWrapperDTO)
}

View file

@ -1,46 +0,0 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@Deprecated(
message = "Use AppPreferencesStore",
replaceWith = ReplaceWith(
expression = "AppPreferencesStore",
imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"),
),
level = DeprecationLevel.WARNING,
)
interface UserTokensStore {
@Deprecated(
message = "Use getObject",
replaceWith = ReplaceWith(
expression = "appPreferencesStore.getObject(userWalletId)",
imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"),
),
level = DeprecationLevel.WARNING,
)
fun get(key: UserWalletId): Flow<UserTokensResponse>
@Deprecated(
message = "Use getObjectSyncOrNull",
replaceWith = ReplaceWith(
expression = "appPreferencesStore.getObjectSyncOrNull(userWalletId)",
imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"),
),
level = DeprecationLevel.WARNING,
)
suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse?
@Deprecated(
message = "Use storeObject",
replaceWith = ReplaceWith(
expression = "appPreferencesStore.storeObject(userWalletId, response)",
imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"),
),
level = DeprecationLevel.WARNING,
)
suspend fun store(key: UserWalletId, value: UserTokensResponse)
}

View file

@ -1,56 +0,0 @@
package com.tangem.datasource.local.token
import androidx.datastore.core.DataMigration
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.files.FileReader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
/**
* Migration of saving [UserTokensResponse] from file to [AppPreferencesStore]
*
* @param userWalletId user wallet id
* @param moshi moshi
* @property fileReader file reader
*
[REDACTED_AUTHOR]
*/
internal class UserTokensStoreMigration(
userWalletId: String,
moshi: Moshi,
private val fileReader: FileReader,
) : DataMigration<AppPreferencesStore> {
private val legacyFileName = "user_tokens_$userWalletId"
private val keyName = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId)
@OptIn(ExperimentalStdlibApi::class)
private val adapter = moshi.adapter<UserTokensResponse>()
override suspend fun shouldMigrate(currentData: AppPreferencesStore): Boolean = true
override suspend fun migrate(currentData: AppPreferencesStore): AppPreferencesStore {
val currentKey = currentData.getObjectSyncOrNull<UserTokensResponse>(key = keyName)
if (currentKey != null) return currentData
val value = runCatching {
val json = fileReader.readFile(legacyFileName)
adapter.fromJson(json)
}.getOrNull()
if (value != null) {
currentData.storeObject(key = keyName, value = value)
}
return currentData
}
override suspend fun cleanUp() {
fileReader.removeFile(legacyFileName)
}
}

View file

@ -1,50 +0,0 @@
package com.tangem.datasource.local.token
import com.squareup.moshi.Moshi
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
/**
* Runner that launch migrations of saving user tokens store
*
* @property appPreferencesStore application preference store
* @property fileReader file reader
* @property moshi moshi
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
@Singleton
class UserTokensStoreMigrationRunner @Inject constructor(
private val appPreferencesStore: AppPreferencesStore,
private val fileReader: FileReader,
@NetworkMoshi private val moshi: Moshi,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend fun run(ids: List<String>) {
ids.forEach { id ->
coroutineScope { run(id) }
}
}
private suspend fun run(id: String) {
withContext(dispatchers.io) {
val migration = UserTokensStoreMigration(
userWalletId = id,
moshi = moshi,
fileReader = fileReader,
)
migration.migrate(appPreferencesStore)
migration.cleanUp()
}
}
}

View file

@ -19,6 +19,7 @@ class DefaultAppComponentContext(
messageHandler: UiMessageHandler,
override val dispatchers: CoroutineDispatcherProvider,
override val hiltComponentBuilder: DecomposeComponent.Builder,
private val replaceRouter: Router? = null,
) : AppComponentContext, ComponentContext by componentContext {
override val tags: HashMap<String, Any> = HashMap()
@ -31,5 +32,5 @@ class DefaultAppComponentContext(
get() = instanceKeeper.getOrCreate { DefaultAppNavigationProvider() }
override val router: Router
get() = instanceKeeper.getOrCreate { DefaultRouter(navigationProvider) }
get() = replaceRouter ?: instanceKeeper.getOrCreate { DefaultRouter(navigationProvider) }
}

View file

@ -100,6 +100,7 @@
<string name="common_enable">Aktivieren</string>
<string name="common_enabled">Aktiviert</string>
<string name="common_error">Fehler</string>
<string name="common_exchange">Umtausch</string>
<string name="common_explore">Erkunden</string>
<string name="common_explore_transaction_history">Transaktionsverlauf einsehen</string>
<string name="common_explorer">Explorer</string>
@ -834,6 +835,8 @@
<string name="wallet_settings_title">Wallet-Einstellungen</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Verwende %s oder scanne eine Karte, um den Zugriff auf deine Wallet freizuschalten.</string>
<string name="warning_approval_in_progress_message">Das Genehmigungsverfahren ist derzeit im Gange und wird in Kürze abgeschlossen sein</string>
<string name="warning_approval_in_progress_title">Genehmigung läuft</string>
<string name="warning_backup_errors_message">Es scheint, dass die Aktivierung der Karte nicht korrekt abgeschlossen wurde. Dies kann an einem Problem mit dem NFC-Modul deines Gerätes oder an einem falschen Tippen der Karte auf dein Gerät liegen. Bitte wende dich an unser Support-Team, um Unterstützung zu erhalten.</string>
<string name="warning_backup_errors_title">Aktivierungsfehler</string>
<string name="warning_beacon_chain_retirement_content">Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen.</string>
@ -859,6 +862,8 @@
<string name="warning_express_not_enough_fee_for_token_tx_description">Um eine Transaktion durchzuführen, du etwas etwas einzahlen %1$s %2$s</string>
<string name="warning_express_not_enough_fee_for_token_tx_title">Die Gebühr %s kann nicht gedeckt werden</string>
<string name="warning_express_notification_invalid_reserve_amount_title">Der zu erhaltende Betrag muss mindestens %s betragen</string>
<string name="warning_express_pair_unavailable_message">Dies kann passieren, weil der Anbieter derzeit nicht in der Lage ist, dein ausgewähltes Paar zu swappen. Bitte warte einen Moment und versuche es später erneut. (Code %@)</string>
<string name="warning_express_pair_unavailable_title">Ausgewähltes Paar vorübergehend nicht verfügbar</string>
<string name="warning_express_refresh_required_title">Service vorübergehend nicht verfügbar</string>
<string name="warning_express_too_maximum_amount_title">Die Menge der zu tauschenden Token darf folgende Werte nicht überschreiten %s</string>
<string name="warning_express_too_minimal_amount_title">Der zu tauschende Betrag muss mindestens %s betragen</string>
@ -873,6 +878,8 @@
<string name="warning_low_signatures_message">Auf dieser Karte sind nur noch %s Unterschriften übrig. Du musst dein gesamtes Guthaben abheben.</string>
<string name="warning_low_signatures_title">Geringe Anzahl von Unterschriften</string>
<string name="warning_manage_tokens_legacy_derivation_message">Token in verschiedenen Netzwerken können unterschiedliche Adressen haben. Überprüfe bei der Überweisung noch einmal, ob deine Adresse mit der des Netzwerks übereinstimmt.</string>
<string name="warning_matic_migration_message">MATIC wird auf POL migriert. Es gibt jedoch keine Frist, und MATIC wird noch nicht abgeschafft. Du kannst MATIC-Token weiterhin verwenden oder sie über eien Exchange gegen POL tauschen.</string>
<string name="warning_matic_migration_title">Migration von MATIC zu POL</string>
<plurals name="warning_missing_derivation_message">
<item quantity="one">Verwende deine Karte, um eine Adresse für das %d-Netz zu erhalten</item>
<item quantity="other">Verwende deine Karte, um mehrere Adressen für die %d-Netzwerke zu erhalten</item>

View file

@ -98,6 +98,7 @@
<string name="common_enable">有効にする</string>
<string name="common_enabled">有効</string>
<string name="common_error">エラー</string>
<string name="common_exchange">交換</string>
<string name="common_explore">移動する</string>
<string name="common_explore_transaction_history">取引履歴を調べる</string>
<string name="common_explorer">エクスプローラー</string>
@ -822,6 +823,8 @@
<string name="wallet_settings_title">ウォレット設定</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">%sを使用するか、カードをスキャンしてウォレットにアクセスしてください</string>
<string name="warning_approval_in_progress_message">許可付与のプロセスは現在進行中であり、まもなく完了する予定です。</string>
<string name="warning_approval_in_progress_title">承認中</string>
<string name="warning_backup_errors_message">カードのアクティベーションが正しく完了しませんでした。デバイスの NFCモジュールに問題があるか、カードをデバイスに正しくタップしていないことが原因かもしれません。サポートチームにお問い合わせください。</string>
<string name="warning_backup_errors_title">アクティベーションに失敗しました</string>
<string name="warning_beacon_chain_retirement_content">BNBネットワーク開発者によると、BEP-2規格のサポートは2024年6月に終了します。この規格の資産を失わないために、BEP-20規格に変換してください。BNBスマートチェーンネットワークへ移行するには、Tangemのスワップサービスをご利用ください。</string>

View file

@ -683,7 +683,7 @@
<string name="staking_notification_earn_rewards_text_period_week">Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждую неделю.</string>
<string name="staking_notification_earn_rewards_title">Получите награду за стейкинг</string>
<string name="staking_notification_unstake_text">Награда перестанет начисляться сразу после завершения стейкинга. Процесс завершения длится %s.</string>
<string name="staking_restake">Повторный стейкинг</string>
<string name="staking_restake">Сменить валидатора</string>
<string name="staking_restake_rewards">Застейкать вознаграждения</string>
<string name="staking_revoke">Отозвать</string>
<string name="staking_revote">Переголосовать</string>
@ -701,6 +701,7 @@
<string name="staking_stake_locked">Стейкинг закрыт</string>
<string name="staking_stake_more">Застейкать еще</string>
<string name="staking_title_stake">Застейкать %s</string>
<string name="staking_title_unstake">Вывести %s</string>
<string name="staking_unlocked_locked">Разблокировать</string>
<string name="staking_unstaked">Выведено из стейкинга</string>
<string name="staking_unstaked_footer">Проверьте процесс завершения стейкинга, чтобы вывести свои средства.</string>
@ -858,8 +859,6 @@
<string name="warning_existential_deposit_title">Для работы с сетью необходим депозит</string>
<string name="warning_express_active_transaction_message">Обмен будет доступен после завершения %s транзакции</string>
<string name="warning_express_active_transaction_title">У вас есть активная транзакция</string>
<string name="warning_express_approval_in_progress_message">Разрешение обмена в процессе и будет скоро завершено</string>
<string name="warning_express_approval_in_progress_title">Разрешение в процессе</string>
<string name="warning_express_dust_message">Минимальная сумма обмена - %1$s. Пожалуйста, убедитесь, что остаток после обмена также не будет меньше %2$s.</string>
<string name="warning_express_no_exchangeable_coins_description">У вас в списке нет монет доступных для обмена с %s</string>
<string name="warning_express_no_exchangeable_coins_title">Нет доступных для обмена токенов</string>

View file

@ -703,7 +703,7 @@
<string name="staking_reward_schedule_month">Місяць</string>
<string name="staking_reward_schedule_week">Тиждень</string>
<string name="staking_rewards">Винагороди</string>
<string name="staking_stake_locked">Застейкати</string>
<string name="staking_stake_locked">Стейкінг закрито</string>
<string name="staking_stake_more">Застейкати більше</string>
<string name="staking_title_stake">Застейкати %s</string>
<string name="staking_title_unstake">Зняти зі стейкінгу %s</string>
@ -843,6 +843,8 @@
<string name="wallet_settings_title">Налаштування гаманця</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Використовуйте %s або відскануйте картку, щоб розблокувати доступ до гаманця</string>
<string name="warning_approval_in_progress_message">Затвердження обміну триває і незабаром буде завершено</string>
<string name="warning_approval_in_progress_title">Затвердження в процесі</string>
<string name="warning_backup_errors_message">Схоже, що активація картки була виконана неправильно. Це може бути пов\'язано з проблемою з модулем NFC вашого пристрою або неправильним прикладанням картки до пристрою. Зверніться за допомогою до нашої служби підтримки.</string>
<string name="warning_backup_errors_title">Помилка активації</string>
<string name="warning_beacon_chain_retirement_content">За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain.</string>
@ -860,14 +862,13 @@
<string name="warning_existential_deposit_title">Для роботи з мережею вимагається депозит</string>
<string name="warning_express_active_transaction_message">Обмін буде доступний після завершення %s транзакції</string>
<string name="warning_express_active_transaction_title">У вас є активна транзакція</string>
<string name="warning_express_approval_in_progress_message">Затвердження обміну триває і незабаром буде завершено</string>
<string name="warning_express_approval_in_progress_title">Затвердження в процесі</string>
<string name="warning_express_dust_message">Мінімальна сума обміну становить - %1$s. Будь ласка, переконайтеся, що залишок на рахунку після обміну буде не менше за %2$s.</string>
<string name="warning_express_no_exchangeable_coins_description">У вашому списку немає доступних монет для обміну %s</string>
<string name="warning_express_no_exchangeable_coins_title">Немає доступних токенів для обміну</string>
<string name="warning_express_not_enough_fee_for_token_tx_description">Щоб здійснити транзакцію, вам потрібно внести трохи %1$s %2$s</string>
<string name="warning_express_not_enough_fee_for_token_tx_title">Неможливо покрити комісію %s</string>
<string name="warning_express_notification_invalid_reserve_amount_title">Сума отримання не може бути меншою за %s</string>
<string name="warning_express_pair_unavailable_message">Це може статися тому, що провайдер наразі не може обміняти обрану пару. Будь ласка, зачекайте трохи та спробуйте ще раз. (Код %@)</string>
<string name="warning_express_refresh_required_title">Сервіс тимчасово недоступний</string>
<string name="warning_express_too_maximum_amount_title">Сума до обміну не повинна перевищувати %s</string>
<string name="warning_express_too_minimal_amount_title">Сума для обміну має бути не менше %s</string>

View file

@ -63,6 +63,7 @@
<string name="common_no"></string>
<string name="common_ok">OK</string>
<string name="common_origin_card">主卡片</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_reject">拒絕</string>
<string name="common_rename">重新命名</string>
<string name="common_save_changes">保存設置</string>

View file

@ -114,6 +114,10 @@
<string name="common_go_to_provider">Go to provider</string>
<string name="common_go_to_token">Go to token</string>
<string name="common_import">Import</string>
<plurals name="common_in_days">
<item quantity="one">in %d day</item>
<item quantity="other">in %d days</item>
</plurals>
<string name="common_later">Later</string>
<string name="common_locked">Locked</string>
<string name="common_main_network">Main network</string>
@ -655,12 +659,12 @@
<string name="staking_details_market_rating">Market rating</string>
<string name="staking_details_metrics_block_header">Metrics</string>
<string name="staking_details_minimum_requirement">Minimum Requirement</string>
<string name="staking_details_no_rewards_to_claim">No rewards to claim</string>
<string name="staking_details_no_rewards_to_claim">No rewards</string>
<string name="staking_details_reward_claiming">Reward claiming</string>
<string name="staking_details_reward_claiming_info">Method of receiving staking rewards.\nIt can be either automatic, where the reward is credited to your address, or manual, where you need to withdraw the reward by creating a transaction to receive it.</string>
<string name="staking_details_reward_schedule">Reward schedule</string>
<string name="staking_details_reward_schedule_info">This is a schedule that determines when participants in staking receive their rewards.</string>
<string name="staking_details_rewards_to_claim">Rewards to claim: %s</string>
<string name="staking_details_rewards_to_claim">Rewards: %s</string>
<string name="staking_details_title">Staking %s</string>
<string name="staking_details_unbonding_period">Unbonding Period</string>
<string name="staking_details_unbonding_period_info">The period you must wait after requesting to withdraw funds from staking before the tokens become available.</string>
@ -668,12 +672,13 @@
<string name="staking_details_warmup_period_info">The allocated time for activating participation in staking.</string>
<string name="staking_migrate">Migrate</string>
<string name="staking_native">Native staking</string>
<string name="staking_notification_earn_rewards_text_period_day">Staking allow you to earn %1$s. Your staking rewards arrive every day.</string>
<string name="staking_notification_earn_rewards_text_period_hour">Staking allow you to earn %1$s. Your staking rewards arrive every hour.</string>
<string name="staking_notification_earn_rewards_text_period_month">Staking allow you to earn %1$s. Your staking rewards arrive every month.</string>
<string name="staking_notification_earn_rewards_text_period_week">Staking allow you to earn %1$s. Your staking rewards arrive every week.</string>
<string name="staking_notification_earn_rewards_text_period_day">Staking allows you to earn %1$s. Your staking rewards arrive every day.</string>
<string name="staking_notification_earn_rewards_text_period_hour">Staking allows you to earn %1$s. Your staking rewards arrive every hour.</string>
<string name="staking_notification_earn_rewards_text_period_month">Staking allows you to earn %1$s. Your staking rewards arrive every month.</string>
<string name="staking_notification_earn_rewards_text_period_week">Staking allows you to earn %1$s. Your staking rewards arrive every week.</string>
<string name="staking_notification_earn_rewards_title">Earn staking rewards</string>
<string name="staking_notification_unstake_text">Rewards stop accruing immediately after you unstake. The unstaking process takes %s.</string>
<string name="staking_notification_unstake_text">Rewards stop accruing immediately after you start unstaking. The unstaking process takes %s.</string>
<string name="staking_ready_to_withdraw">Ready to withdraw</string>
<string name="staking_rebond">Rebond</string>
<string name="staking_restake">Restake</string>
<string name="staking_restake_rewards">Restake rewards</string>
@ -694,6 +699,7 @@
<string name="staking_stake_more">Stake more</string>
<string name="staking_title_stake">Stake %s</string>
<string name="staking_title_unstake">Unstake %s</string>
<string name="staking_unbonding">Unbonding</string>
<string name="staking_unlocked_locked">Unlock locked</string>
<string name="staking_unstaked">Unstaked</string>
<string name="staking_unstaked_footer">Check unstaked to claim your assets</string>
@ -875,6 +881,8 @@
<string name="warning_low_signatures_message">Only %s signatures are left on this card. You must withdraw all of your funds.</string>
<string name="warning_low_signatures_title">Low signature count</string>
<string name="warning_manage_tokens_legacy_derivation_message">Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds.</string>
<string name="warning_matic_migration_message">MATIC is being migrated to POL. However there is no deadline set and MATIC isn\'t being deprecated yet. You can safely continue using MATIC token or use exchanges to swap it for POL.</string>
<string name="warning_matic_migration_title">MATIC to POL Migration</string>
<plurals name="warning_missing_derivation_message">
<item quantity="one">Use your card to get an address for %d network</item>
<item quantity="other">Use your card to get an addresses for %d networks</item>

View file

@ -40,6 +40,7 @@ dependencies {
implementation(deps.compose.coil)
implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt)
implementation(deps.compose.reorderable)
/** Other libraries */
implementation(deps.compose.accompanist.systemUiController)

View file

@ -0,0 +1,22 @@
package com.tangem.core.ui.coil
import android.graphics.Bitmap
import android.graphics.Matrix
import coil.size.Size
import coil.transform.Transformation
class RotationTransformation(private val angle: Float) : Transformation {
override val cacheKey: String = "rotate:$angle"
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
val matrix = Matrix().apply {
val centerX = input.width / 2f
val centerY = input.height / 2f
postRotate(angle, centerX, centerY)
}
return Bitmap.createBitmap(input, 0, 0, input.width, input.height, matrix, true)
}
}

View file

@ -49,10 +49,10 @@ import kotlinx.collections.immutable.toImmutableList
@Composable
fun BasicDialog(
message: String,
confirmButton: DialogButton,
confirmButton: DialogButtonUM,
onDismissDialog: () -> Unit,
title: String? = null,
dismissButton: DialogButton? = null,
dismissButton: DialogButtonUM? = null,
isDismissable: Boolean = true,
) {
TangemDialog(
@ -72,7 +72,7 @@ fun BasicDialog(
fun SimpleOkDialog(message: String, onDismissDialog: () -> Unit) {
TangemDialog(
type = DialogType.Message(message),
confirmButton = DialogButton(onClick = onDismissDialog),
confirmButton = DialogButtonUM(onClick = onDismissDialog),
onDismissDialog = onDismissDialog,
title = null,
dismissButton = null,
@ -97,12 +97,12 @@ fun SimpleOkDialog(message: String, onDismissDialog: () -> Unit) {
@Composable
fun TextInputDialog(
fieldValue: TextFieldValue,
confirmButton: DialogButton,
confirmButton: DialogButtonUM,
onDismissDialog: () -> Unit,
onValueChange: (TextFieldValue) -> Unit,
textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() },
textFieldParams: AdditionalTextInputDialogUM = remember { AdditionalTextInputDialogUM() },
title: String? = null,
dismissButton: DialogButton? = null,
dismissButton: DialogButtonUM? = null,
isDismissable: Boolean = true,
) {
TangemDialog(
@ -125,12 +125,12 @@ fun TextInputDialog(
@Composable
fun TextInputDialog(
fieldValue: String,
confirmButton: DialogButton,
confirmButton: DialogButtonUM,
onDismissDialog: () -> Unit,
onValueChange: (String) -> Unit,
textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() },
textFieldParams: AdditionalTextInputDialogUM = remember { AdditionalTextInputDialogUM() },
title: String? = null,
dismissButton: DialogButton? = null,
dismissButton: DialogButtonUM? = null,
isDismissable: Boolean = true,
) {
TangemDialog(
@ -154,7 +154,7 @@ fun TextInputDialog(
fun SelectorDialog(
selectedItemIndex: Int,
items: ImmutableList<String>,
confirmButton: DialogButton,
confirmButton: DialogButtonUM,
onSelect: (index: Int) -> Unit,
onDismissDialog: () -> Unit,
title: String? = null,
@ -180,7 +180,7 @@ fun SelectorDialog(
* @param enabled If false button will be disabled
* @param onClick Button click callback
*/
data class DialogButton(
data class DialogButtonUM(
val title: String? = null,
val warning: Boolean = false,
val enabled: Boolean = true,
@ -190,7 +190,7 @@ data class DialogButton(
/**
* Additional params for dialog text field
*/
data class AdditionalTextInputDialogParams(
data class AdditionalTextInputDialogUM(
val label: String? = null,
val placeholder: String? = null,
val caption: String? = null,
@ -203,10 +203,10 @@ data class AdditionalTextInputDialogParams(
@Composable
private fun TangemDialog(
type: DialogType,
confirmButton: DialogButton,
confirmButton: DialogButtonUM,
onDismissDialog: () -> Unit,
title: String? = null,
dismissButton: DialogButton? = null,
dismissButton: DialogButtonUM? = null,
properties: DialogProperties = DialogProperties(),
) {
Dialog(properties = properties, onDismissRequest = onDismissDialog) {
@ -304,7 +304,11 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
}
@Composable
private fun DialogButtons(confirmButton: DialogButton, dismissButton: DialogButton?, modifier: Modifier = Modifier) {
private fun DialogButtons(
confirmButton: DialogButtonUM,
dismissButton: DialogButtonUM?,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(
@ -413,13 +417,13 @@ private sealed class DialogType {
data class TextInput(
val value: TextFieldValue,
val onValueChange: (TextFieldValue) -> Unit,
val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(),
val params: AdditionalTextInputDialogUM = AdditionalTextInputDialogUM(),
) : DialogType()
data class SimpleTextInput(
val value: String,
val onValueChange: (String) -> Unit,
val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(),
val params: AdditionalTextInputDialogUM = AdditionalTextInputDialogUM(),
) : DialogType()
data class Selector(
@ -445,8 +449,8 @@ private fun BasicDialogPreview() {
message = "All protected passwords will be deleted from the secure storage, you must enter the wallet " +
"password to work with the app",
title = "Attention",
confirmButton = DialogButton {},
dismissButton = DialogButton {},
confirmButton = DialogButtonUM {},
dismissButton = DialogButtonUM {},
onDismissDialog = {},
)
}
@ -478,8 +482,8 @@ private fun WarningBasicDialogSample(modifier: Modifier = Modifier) {
message = "All protected passwords will be deleted from the secure storage, you must enter the wallet " +
"password to work with the app",
title = "Attention",
confirmButton = DialogButton(warning = true) {},
dismissButton = DialogButton {},
confirmButton = DialogButtonUM(warning = true) {},
dismissButton = DialogButtonUM {},
onDismissDialog = {},
)
}
@ -502,10 +506,10 @@ private fun TextInputDialogSample(modifier: Modifier = Modifier) {
TextInputDialog(
fieldValue = TextFieldValue(text = ""),
title = "Rename Wallet",
confirmButton = DialogButton {},
confirmButton = DialogButtonUM {},
onDismissDialog = {},
onValueChange = {},
textFieldParams = AdditionalTextInputDialogParams(
textFieldParams = AdditionalTextInputDialogUM(
label = "Wallet name",
),
)
@ -530,7 +534,7 @@ private fun SelectorDialogPreview(@PreviewParameter(SelctorDialogParamsProvider:
title = param.title,
items = param.items,
selectedItemIndex = param.selectedItemIndex,
confirmButton = DialogButton(title = "Cancel", onClick = {}),
confirmButton = DialogButtonUM(title = "Cancel", onClick = {}),
onSelect = {},
onDismissDialog = {},
)

View file

@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
@ -101,7 +102,11 @@ fun TextShimmer(
* Height and min width will be set automatically
*/
@Composable
fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false) {
fun SmallButtonShimmer(
modifier: Modifier = Modifier,
shape: Shape = RoundedCornerShape(size = TangemTheme.dimens.radius16),
withIcon: Boolean = false,
) {
PrimarySmallButton(
config = SmallButtonConfig(
text = stringReference("B"),
@ -113,7 +118,7 @@ fun SmallButtonShimmer(modifier: Modifier = Modifier, withIcon: Boolean = false)
},
),
modifier = modifier
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius16))
.clip(shape)
.shimmer(LocalTangemShimmer.current),
)
}

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
@ -25,10 +26,12 @@ import kotlinx.collections.immutable.persistentListOf
@Immutable
class InformationBlockContentScope(val scope: BoxScope) : BoxScope by scope
// TODO: [REDACTED_JIRA]
@Composable
fun InformationBlock(
title: @Composable BoxScope.() -> Unit,
modifier: Modifier = Modifier,
contentHorizontalPadding: Dp = TangemTheme.dimens.spacing12,
action: (@Composable BoxScope.() -> Unit)? = null,
content: (@Composable InformationBlockContentScope.() -> Unit)? = null,
) {
@ -38,15 +41,28 @@ fun InformationBlock(
.background(color = TangemTheme.colors.background.action),
horizontalAlignment = Alignment.Start,
) {
val minHeight = if (action == null) TangemTheme.dimens.size36 else TangemTheme.dimens.size40
val padding = if (action == null) {
PaddingValues(
start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing4,
)
} else {
PaddingValues(
start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing11,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing5,
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size40)
.padding(
top = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing6,
)
.padding(horizontal = TangemTheme.dimens.spacing12),
.heightIn(min = minHeight)
.padding(paddingValues = padding),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@ -72,7 +88,7 @@ fun InformationBlock(
if (content != null) {
Box(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing12)
.padding(horizontal = contentHorizontalPadding)
.fillMaxWidth(),
) {
val scope = InformationBlockContentScope(scope = this)

View file

@ -33,6 +33,7 @@ data class SmallButtonConfig(
val text: TextReference,
val onClick: () -> Unit,
val icon: TangemButtonIconPosition = TangemButtonIconPosition.None,
val enabled: Boolean = true,
)
/**
@ -57,6 +58,7 @@ fun SecondarySmallButton(config: SmallButtonConfig, modifier: Modifier = Modifie
SmallButton(config = config, isPrimary = false, modifier = modifier)
}
@Suppress("LongMethod")
@Composable
private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: Modifier = Modifier) {
val shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)
@ -77,7 +79,7 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
color = backgroundColor,
shape = shape,
)
.clickable(enabled = true, onClick = config.onClick)
.clickable(enabled = config.enabled, onClick = config.onClick)
.padding(
paddingValues = when (config.icon) {
is TangemButtonIconPosition.None -> PaddingValues(
@ -100,7 +102,11 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
iconPosition = config.icon,
text = {
val textColor by animateColorAsState(
targetValue = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1,
targetValue = when {
!config.enabled -> TangemTheme.colors.text.disabled
isPrimary -> TangemTheme.colors.text.primary2
else -> TangemTheme.colors.text.primary1
},
label = "Update text color",
)
@ -116,7 +122,11 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = iconResId),
tint = TangemTheme.colors.icon.secondary,
tint = if (config.enabled) {
TangemTheme.colors.icon.secondary
} else {
TangemTheme.colors.icon.inactive
},
contentDescription = null,
)
},
@ -174,5 +184,12 @@ private fun ButtonsSample() {
icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24),
),
)
SecondarySmallButton(
config = config.copy(
text = TextReference.Str(value = "Add token"),
icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24),
enabled = false,
),
)
}
}

View file

@ -1,5 +1,10 @@
package com.tangem.core.ui.components.buttons.common
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
@ -36,6 +41,7 @@ fun TangemButton(
textStyle: TextStyle = TangemTheme.typography.button,
shape: Shape = size.toShape(),
iconPadding: Dp = size.toIconPadding(),
animateContentChange: Boolean = false,
) {
val multipleClickPreventer = remember { MultipleClickPreventer.get() }
@ -56,6 +62,7 @@ fun TangemButton(
buttonIcon = icon,
iconPadding = iconPadding,
showProgress = showProgress,
animateContentChange = animateContentChange,
progressIndicator = {
CircularProgressIndicator(
modifier = Modifier.buttonContentSize(maxContentSize),
@ -108,24 +115,54 @@ private inline fun RowScope.ButtonContentContainer(
buttonIcon: TangemButtonIconPosition,
iconPadding: Dp,
showProgress: Boolean,
animateContentChange: Boolean,
progressIndicator: @Composable RowScope.() -> Unit,
text: @Composable RowScope.() -> Unit,
icon: @Composable RowScope.(Int) -> Unit,
crossinline text: @Composable RowScope.() -> Unit,
crossinline icon: @Composable RowScope.(Int) -> Unit,
additionalText: @Composable () -> Unit,
) {
if (showProgress) {
progressIndicator()
} else {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row(horizontalArrangement = Arrangement.Center) {
if (buttonIcon is TangemButtonIconPosition.Start) {
icon(buttonIcon.iconResId)
Spacer(modifier = Modifier.requiredWidth(iconPadding))
if (animateContentChange) {
AnimatedContent(
targetState = buttonIcon,
transitionSpec = {
fadeIn(tween(durationMillis = 220)) togetherWith
fadeOut(tween(durationMillis = 220))
},
label = "button text with icon",
) { iconState ->
Row(horizontalArrangement = Arrangement.Center) {
when (iconState) {
is TangemButtonIconPosition.Start -> {
icon(iconState.iconResId)
Spacer(modifier = Modifier.requiredWidth(iconPadding))
text()
}
is TangemButtonIconPosition.End -> {
text()
Spacer(modifier = Modifier.requiredWidth(iconPadding))
icon(iconState.iconResId)
}
is TangemButtonIconPosition.None -> {
text()
}
}
}
}
text()
if (buttonIcon is TangemButtonIconPosition.End) {
Spacer(modifier = Modifier.requiredWidth(iconPadding))
icon(buttonIcon.iconResId)
} else {
Row(horizontalArrangement = Arrangement.Center) {
if (buttonIcon is TangemButtonIconPosition.Start) {
icon(buttonIcon.iconResId)
Spacer(modifier = Modifier.requiredWidth(iconPadding))
}
text()
if (buttonIcon is TangemButtonIconPosition.End) {
Spacer(modifier = Modifier.requiredWidth(iconPadding))
icon(buttonIcon.iconResId)
}
}
}
additionalText()

View file

@ -38,7 +38,7 @@ sealed class CurrencyIconState {
* Represents a token icon.
*
* @property url The URL where the token icon can be fetched from. May be `null` if not found.
* @property topBadgeIconResId The drawable resource ID for the network badge.
* @property topBadgeIconResId The drawable resource ID for the network badge. May be `null`.
* @property isGrayscale Specifies whether to show the icon in grayscale.
* @property showCustomBadge Specifies whether to show the custom token badge.
* @property fallbackTint The color to be used for tinting the fallback icon.
@ -46,7 +46,7 @@ sealed class CurrencyIconState {
*/
data class TokenIcon(
val url: String?,
@DrawableRes override val topBadgeIconResId: Int,
@DrawableRes override val topBadgeIconResId: Int?,
override val isGrayscale: Boolean,
override val showCustomBadge: Boolean,
val fallbackTint: Color,
@ -81,4 +81,28 @@ sealed class CurrencyIconState {
override val showCustomBadge: Boolean = false
override val topBadgeIconResId: Int? = null
}
fun copySealed(
isGrayscale: Boolean = this.isGrayscale,
showCustomBadge: Boolean = this.showCustomBadge,
topBadgeIconResId: Int? = this.topBadgeIconResId,
): CurrencyIconState = when (this) {
is CoinIcon -> copy(
isGrayscale = isGrayscale,
showCustomBadge = showCustomBadge,
)
is CustomTokenIcon -> copy(
isGrayscale = isGrayscale,
showCustomBadge = showCustomBadge,
topBadgeIconResId = topBadgeIconResId ?: this.topBadgeIconResId,
)
is TokenIcon -> copy(
isGrayscale = isGrayscale,
showCustomBadge = showCustomBadge,
topBadgeIconResId = topBadgeIconResId,
)
is Loading,
is Locked,
-> this
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.core.ui.components.list
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.*
@Composable
fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Boolean, buffer: Int = 2) {
val loadMore by remember {
derivedStateOf {
val layoutInfo = listState.layoutInfo
val totalItemsNumber = layoutInfo.totalItemsCount
val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1
lastVisibleItemIndex > totalItemsNumber - buffer
}
}
val totalItemsCount by remember { derivedStateOf { listState.layoutInfo.totalItemsCount } }
var emitted by remember(totalItemsCount) { mutableStateOf(false) }
LaunchedEffect(loadMore) {
if (loadMore && !emitted) {
emitted = onLoadMore()
}
}
}

View file

@ -1,6 +1,21 @@
package com.tangem.core.ui.components.marketprice
import java.math.BigDecimal
import java.math.RoundingMode
/** Price changing type */
enum class PriceChangeType {
UP, DOWN, NEUTRAL,
;
companion object {
@Suppress("MagicNumber")
fun fromBigDecimal(priceChangePercent: BigDecimal): PriceChangeType {
return when {
priceChangePercent < BigDecimal.ZERO -> DOWN
priceChangePercent.setScale(4, RoundingMode.HALF_UP) > BigDecimal.ZERO -> UP
else -> NEUTRAL
}
}
}
}

View file

@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.*
@ -68,6 +69,7 @@ private class ChildArrowScope(
@Composable
fun ChildArrow(childHeight: Dp, isLastChild: Boolean) {
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
val figureWidth = TangemTheme.dimens.size40
val strokeColor = TangemTheme.colors.stroke.secondary
@ -86,18 +88,31 @@ fun ChildArrow(childHeight: Dp, isLastChild: Boolean) {
)
val arrowHeadRectDp = DpRect(
origin = DpOffset(
x = figureWidth - arrowHeadSize.width,
x = if (isLtr) {
figureWidth - arrowHeadSize.width
} else {
0.dp
},
y = figureRectDp.size.center.y - arrowHeadSize.center.y,
),
size = arrowHeadSize,
)
val curvedArrowRectDp = DpRect(
top = figureRectDp.top,
left = TangemTheme.dimens.size18,
right = figureRectDp.right - arrowHeadRectDp.width,
bottom = figureRectDp.size.center.y,
)
val curvedArrowRectDp = if (isLtr) {
DpRect(
top = figureRectDp.top,
left = TangemTheme.dimens.size18,
right = figureRectDp.right - arrowHeadRectDp.width,
bottom = figureRectDp.size.center.y,
)
} else {
DpRect(
top = figureRectDp.top,
left = arrowHeadRectDp.width,
right = TangemTheme.dimens.size18 + arrowHeadRectDp.width,
bottom = figureRectDp.size.center.y,
)
}
Canvas(
modifier = Modifier
@ -114,20 +129,26 @@ fun ChildArrow(childHeight: Dp, isLastChild: Boolean) {
drawScope = this,
)
scope.drawCurveArrow()
scope.drawArrowHead()
scope.drawCurveArrow(isLtr)
scope.drawArrowHead(isLtr)
if (!isLastChild) {
scope.drawArrowLine()
scope.drawArrowLine(isLtr)
}
}
}
private fun ChildArrowScope.drawArrowHead() {
private fun ChildArrowScope.drawArrowHead(isLtr: Boolean) {
val arrowHeadPath = Path().apply {
moveTo(arrowHeadRect.centerRight)
lineTo(arrowHeadRect.topLeft)
lineTo(arrowHeadRect.bottomLeft)
if (isLtr) {
moveTo(arrowHeadRect.centerRight)
lineTo(arrowHeadRect.topLeft)
lineTo(arrowHeadRect.bottomLeft)
} else {
moveTo(arrowHeadRect.centerLeft)
lineTo(arrowHeadRect.topRight)
lineTo(arrowHeadRect.bottomRight)
}
close()
}
val paint = Paint().apply {
@ -143,13 +164,21 @@ private fun ChildArrowScope.drawArrowHead() {
}
}
private fun ChildArrowScope.drawCurveArrow() {
private fun ChildArrowScope.drawCurveArrow(isLtr: Boolean) {
val curveArrowPath = Path().apply {
moveTo(curvedArrowRect.topLeft)
quadraticBezierTo(
control = curvedArrowRect.bottomLeft,
end = curvedArrowRect.bottomRight,
)
if (isLtr) {
moveTo(curvedArrowRect.topLeft)
quadraticBezierTo(
control = curvedArrowRect.bottomLeft,
end = curvedArrowRect.bottomRight,
)
} else {
moveTo(curvedArrowRect.topRight)
quadraticBezierTo(
control = curvedArrowRect.bottomRight,
end = curvedArrowRect.bottomLeft,
)
}
}
drawPath(
path = curveArrowPath,
@ -158,11 +187,20 @@ private fun ChildArrowScope.drawCurveArrow() {
)
}
private fun ChildArrowScope.drawArrowLine() {
drawLine(
color = strokeColor,
start = curvedArrowRect.topLeft,
end = Offset(curvedArrowRect.left, figureRect.bottom),
strokeWidth = arrowStrokeWidth,
)
private fun ChildArrowScope.drawArrowLine(isLtr: Boolean) {
if (isLtr) {
drawLine(
color = strokeColor,
start = curvedArrowRect.topLeft,
end = Offset(curvedArrowRect.left, figureRect.bottom),
strokeWidth = arrowStrokeWidth,
)
} else {
drawLine(
color = strokeColor,
start = curvedArrowRect.topRight,
end = Offset(curvedArrowRect.right, figureRect.bottom),
strokeWidth = arrowStrokeWidth,
)
}
}

View file

@ -29,8 +29,9 @@ fun BlockchainRow(model: BlockchainRowUM, action: @Composable BoxScope.() -> Uni
modifier = modifier
.heightIn(min = TangemTheme.dimens.size52)
.padding(
vertical = TangemTheme.dimens.spacing8,
horizontal = TangemTheme.dimens.spacing8,
top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing8,
start = TangemTheme.dimens.spacing8,
),
icon = {
RowIcon(
@ -125,7 +126,7 @@ private fun Preview_BlockchainRow(@PreviewParameter(BlockchainRowParameterProvid
BlockchainRow(
model = state,
action = {
TangemSwitch(onCheckedChange = { /* [REDACTED_TODO_COMMENT]*/ }, checked = true)
TangemSwitch(onCheckedChange = { }, checked = true)
},
)
},

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.common.component
package com.tangem.core.ui.components.token
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
@ -16,15 +16,18 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Constraints
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.token.internal.*
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.component.token.*
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import org.burnoutcrew.reorderable.ReorderableLazyListState
import java.util.UUID
import kotlin.math.max
private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3
@ -34,12 +37,43 @@ private enum class LayoutId {
ICON, TITLE, FIAT_AMOUNT, CRYPTO_AMOUNT, CRYPTO_PRICE, NON_FIAT_CONTENT
}
/**
* Token item for non reorderable list
*
* @param state token item state
* @param isBalanceHidden flag that shows/hides balance
* @param modifier modifier
*
* @see <a href = "https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1051-866&t=ew8mbGp2lacuJfFm-4"
* >Figma Component</a>
*/
@Composable
internal fun TokenItem(
fun TokenItem(state: TokenItemState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
TokenItem(
state = state,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
reorderableTokenListState = null,
)
}
/**
* Token item for reorderable list
*
* @param state token item state
* @param isBalanceHidden flag that shows/hides balance
* @param reorderableTokenListState reorderable token list state
* @param modifier modifier
*
* @see <a href = "https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1051-866&t=ew8mbGp2lacuJfFm-4"
* >Figma Component</a>
*/
@Composable
fun TokenItem(
state: TokenItemState,
isBalanceHidden: Boolean,
reorderableTokenListState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
reorderableTokenListState: ReorderableLazyListState? = null,
) {
val betweenRowsMargin = TangemTheme.dimens.spacing2
@ -73,7 +107,7 @@ internal fun TokenItem(
)
TokenPrice(
state = state.cryptoPriceState,
state = state.subtitleState,
modifier = Modifier
.layoutId(layoutId = LayoutId.CRYPTO_PRICE)
.padding(end = TangemTheme.dimens.spacing8),
@ -192,6 +226,10 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
is TokenItemState.Unreachable,
-> {
firstRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width
if (state.subtitleState != null) {
secondRowRemainingFreeSpace = layoutWidthWithoutPaddings - icon.width - nonFiatContent.width
}
}
}
@ -229,7 +267,13 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
y = when (state) {
is TokenItemState.NoAddress,
is TokenItemState.Unreachable,
-> (layoutHeight - title.height).div(other = 2)
-> {
if (state.subtitleState == null) {
(layoutHeight - title.height).div(other = 2)
} else {
verticalPadding
}
}
else -> verticalPadding
},
)
@ -397,8 +441,8 @@ private fun Preview_TokenItem_InLight(@PreviewParameter(TokenItemStateProvider::
private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenItemState>(
collection = listOf(
WalletPreviewData.tokenItemVisibleState.copy(
iconState = WalletPreviewData.coinIconState.copy(showCustomBadge = true),
tokenItemVisibleState.copy(
iconState = coinIconState.copy(showCustomBadge = true),
titleState = TokenItemState.TitleState.Content(
text = "PolygonPolygonPolygonPolygonPolygonPolygon",
hasPending = true,
@ -408,19 +452,122 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
hasStaked = true,
),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,4123123213123123123123123123 MATIC"),
cryptoPriceState = TokenItemState.CryptoPriceState.Content(
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
price = "312 USD",
priceChangePercent = "42.0%",
type = PriceChangeType.UP,
),
),
WalletPreviewData.tokenItemUnreachableState,
WalletPreviewData.tokenItemNoAddressState,
WalletPreviewData.tokenItemDragState,
WalletPreviewData.tokenItemHiddenState,
WalletPreviewData.loadingTokenItemState,
WalletPreviewData.testnetTokenItemVisibleState,
WalletPreviewData.customTokenItemVisibleState,
WalletPreviewData.customTestnetTokenItemVisibleState,
TokenItemState.Unreachable(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
onItemClick = {},
onItemLongClick = {},
),
TokenItemState.Unreachable(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
subtitleState = TokenItemState.SubtitleState.TextContent("Token"),
onItemClick = {},
onItemLongClick = {},
),
TokenItemState.NoAddress(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
onItemLongClick = {},
),
TokenItemState.NoAddress(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
subtitleState = TokenItemState.SubtitleState.TextContent("Token"),
onItemLongClick = {},
),
TokenItemState.Draggable(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "3 172,14 $"),
),
TokenItemState.Content(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = false),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"),
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
price = "312 USD",
priceChangePercent = "2.0%",
type = PriceChangeType.UP,
),
onItemClick = {},
onItemLongClick = {},
),
TokenItemState.Loading(
id = "Loading#1",
iconState = customTokenIconState.copy(isGrayscale = true),
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
),
tokenItemVisibleState.copy(
titleState = TokenItemState.TitleState.Content(text = "Polygon testnet"),
iconState = tokenIconState.copy(isGrayscale = true),
),
tokenItemVisibleState.copy(
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
iconState = customTokenIconState.copy(
tint = TangemColorPalette.White,
background = TangemColorPalette.Black,
),
),
tokenItemVisibleState.copy(
titleState = TokenItemState.TitleState.Content(text = "Polygon"),
iconState = customTokenIconState.copy(isGrayscale = true),
),
),
)
) {
companion object {
val coinIconState
get() = CurrencyIconState.CoinIcon(
url = null,
fallbackResId = R.drawable.img_polygon_22,
isGrayscale = false,
showCustomBadge = false,
)
val tokenIconState
get() = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_polygon_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
showCustomBadge = false,
)
private val customTokenIconState
get() = CurrencyIconState.CustomTokenIcon(
tint = TangemColorPalette.Black,
background = TangemColorPalette.Meadow,
topBadgeIconResId = R.drawable.img_polygon_22,
isGrayscale = false,
)
val tokenItemVisibleState by lazy {
TokenItemState.Content(
id = UUID.randomUUID().toString(),
iconState = coinIconState,
titleState = TokenItemState.TitleState.Content(text = "Polygon", hasPending = true),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = true),
cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "5,412 MATIC"),
subtitleState = TokenItemState.SubtitleState.Unknown,
onItemClick = {},
onItemLongClick = {},
)
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.common.component.token
package com.tangem.core.ui.components.token.internal
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.common.component.token
package com.tangem.core.ui.components.token.internal
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedContent
@ -12,9 +12,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.common.component.token
package com.tangem.core.ui.components.token.internal
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -8,11 +8,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.utils.StringsSigns
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoAmountState as TokenCryptoAmountState
import com.tangem.core.ui.components.token.state.TokenItemState.CryptoAmountState as TokenCryptoAmountState
@Composable
internal fun TokenCryptoAmount(

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.common.component.token
package com.tangem.core.ui.components.token.internal
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Row
@ -14,11 +14,11 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.utils.StringsSigns
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.FiatAmountState as TokenFiatAmountState
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState
@Composable
internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.common.component.token
package com.tangem.core.ui.components.token.internal
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
@ -12,19 +13,23 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.SpacerW6
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoPriceState as TokenPriceChangeState
import com.tangem.core.ui.components.token.state.TokenItemState.SubtitleState as TokenPriceState
@Composable
internal fun TokenPrice(state: TokenPriceChangeState?, modifier: Modifier = Modifier) {
internal fun TokenPrice(state: TokenPriceState?, modifier: Modifier = Modifier) {
when (state) {
is TokenPriceChangeState.Content -> {
is TokenPriceState.CryptoPriceContent -> {
PriceBlock(
modifier = modifier,
price = state.price,
@ -32,13 +37,12 @@ internal fun TokenPrice(state: TokenPriceChangeState?, modifier: Modifier = Modi
priceChangePercent = state.priceChangePercent,
)
}
is TokenPriceChangeState.Unknown -> {
PriceText(text = DASH_SIGN, modifier = modifier)
}
is TokenPriceChangeState.Loading -> {
is TokenPriceState.TextContent -> PriceText(text = state.value, modifier = modifier)
is TokenPriceState.Unknown -> PriceText(text = DASH_SIGN, modifier = modifier)
is TokenPriceState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
}
is TokenPriceChangeState.Locked -> {
is TokenPriceState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize())
}
null -> Unit
@ -122,4 +126,37 @@ private fun Modifier.placeholderSize(): Modifier = composed {
return@composed this
.padding(vertical = TangemTheme.dimens.spacing2)
.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview(@PreviewParameter(TokenPriceChangeStateProvider::class) state: TokenPriceState) {
TangemThemePreview {
TokenPrice(state = state)
}
}
private class TokenPriceChangeStateProvider : CollectionPreviewParameterProvider<TokenPriceState>(
collection = listOf(
TokenPriceState.CryptoPriceContent(
price = "1.234",
priceChangePercent = "2.5%",
type = PriceChangeType.UP,
),
TokenPriceState.CryptoPriceContent(
price = "1.234",
priceChangePercent = "2.5%",
type = PriceChangeType.DOWN,
),
TokenPriceState.CryptoPriceContent(
price = "1.234",
priceChangePercent = "2.5%",
type = PriceChangeType.NEUTRAL,
),
TokenPriceState.TextContent(value = "Subtitle"),
TokenPriceState.Unknown,
TokenPriceState.Loading,
TokenPriceState.Locked,
),
)

View file

@ -1,4 +1,4 @@
package com.tangem.feature.wallet.presentation.common.component.token
package com.tangem.core.ui.components.token.internal
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
@ -13,10 +13,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TitleState as TokenTitleState
import com.tangem.core.ui.components.token.state.TokenItemState.TitleState as TokenTitleState
@Composable
internal fun TokenTitle(state: TokenTitleState?, modifier: Modifier = Modifier) {

View file

@ -1,61 +1,81 @@
package com.tangem.feature.wallet.presentation.common.state
package com.tangem.core.ui.components.token.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
/** Token item state */
/** TokenItem component state */
@Immutable
internal sealed class TokenItemState {
sealed class TokenItemState {
/** Unique id */
abstract val id: String
/** Token icon state */
abstract val iconState: CurrencyIconState
/** Token title state (in one row with [fiatAmountState]) */
abstract val titleState: TitleState
/** Token subtitle state (under [titleState] and in one row with [cryptoAmountState]) */
abstract val subtitleState: SubtitleState?
/** Token fiat amount state (in one row with [titleState]) */
abstract val fiatAmountState: FiatAmountState?
/** Token crypto amount state (under [fiatAmountState] and in one row with [subtitleState]) */
abstract val cryptoAmountState: CryptoAmountState?
abstract val cryptoPriceState: CryptoPriceState?
/** Loading token state */
/**
* Loading token state
*
* @property id unique id
* @property iconState token icon state
* @property titleState token title
* @property subtitleState token subtitle
*/
data class Loading(
override val id: String,
override val iconState: CurrencyIconState,
override val titleState: TitleState.Content,
override val subtitleState: SubtitleState = SubtitleState.Loading,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState = FiatAmountState.Loading
override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Loading
override val cryptoPriceState: CryptoPriceState = CryptoPriceState.Loading
}
/** Locked token state */
/**
* Locked token state
*
* @property id unique id
*/
data class Locked(override val id: String) : TokenItemState() {
override val iconState: CurrencyIconState = CurrencyIconState.Locked
override val titleState: TitleState = TitleState.Locked
override val subtitleState: SubtitleState = SubtitleState.Locked
override val fiatAmountState: FiatAmountState = FiatAmountState.Locked
override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked
override val cryptoPriceState: CryptoPriceState = CryptoPriceState.Locked
}
/**
* Content token state
*
* @property id unique id
* @property iconState token icon state
* @property titleState token name
* @property onItemClick callback which will be called when an item is clicked
* @property onItemLongClick callback which will be called when an item is long clicked
* @property id unique id
* @property iconState token icon state
* @property titleState token title
* @property subtitleState token subtitle
* @property fiatAmountState token fiat amount
* @property cryptoAmountState token crypto amount
* @property onItemClick callback which will be called when an item is clicked
* @property onItemLongClick callback which will be called when an item is long clicked
*/
data class Content(
override val id: String,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
override val subtitleState: SubtitleState,
override val fiatAmountState: FiatAmountState,
override val cryptoAmountState: CryptoAmountState.Content,
override val cryptoPriceState: CryptoPriceState,
val onItemClick: () -> Unit,
val onItemLongClick: () -> Unit,
) : TokenItemState()
@ -63,9 +83,10 @@ internal sealed class TokenItemState {
/**
* Draggable token state
*
* @property id unique id
* @property iconState token icon state
* @property titleState token name
* @property id unique id
* @property iconState token icon state
* @property titleState token title
* @property cryptoAmountState token crypto amount
*/
data class Draggable(
override val id: String,
@ -73,48 +94,50 @@ internal sealed class TokenItemState {
override val titleState: TitleState,
override val cryptoAmountState: CryptoAmountState,
) : TokenItemState() {
override val subtitleState: SubtitleState? = null
override val fiatAmountState: FiatAmountState? = null
override val cryptoPriceState: CryptoPriceState? = null
}
/**
* Unreachable token state
*
* @property id token id
* @property iconState token icon state
* @property titleState token name
* @property onItemClick callback which will be called when an item is clicked
* @property onItemLongClick callback which will be called when an item is long clicked
* @property id unique id
* @property iconState token icon state
* @property titleState token title
* @property subtitleState token subtitle
* @property onItemClick callback which will be called when an item is clicked
* @property onItemLongClick callback which will be called when an item is long clicked
*/
data class Unreachable(
override val id: String,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
override val subtitleState: SubtitleState? = null,
val onItemClick: () -> Unit,
val onItemLongClick: () -> Unit,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState? = null
override val cryptoAmountState: CryptoAmountState? = null
override val cryptoPriceState: CryptoPriceState? = null
}
/**
* No derivation address state
*
* @property id token id
* @property iconState token icon state
* @property titleState token name
* @property onItemLongClick callback which will be called when an item is long clicked
* @property id unique id
* @property iconState token icon state
* @property titleState token title
* @property subtitleState token subtitle
* @property onItemLongClick callback which will be called when an item is long clicked
*/
data class NoAddress(
override val id: String,
override val iconState: CurrencyIconState,
override val titleState: TitleState,
override val subtitleState: SubtitleState? = null,
val onItemLongClick: () -> Unit,
) : TokenItemState() {
override val fiatAmountState: FiatAmountState? = null
override val cryptoAmountState: CryptoAmountState? = null
override val cryptoPriceState: CryptoPriceState? = null
}
@Immutable
@ -122,9 +145,27 @@ internal sealed class TokenItemState {
data class Content(val text: String, val hasPending: Boolean = false) : TitleState()
object Loading : TitleState()
data object Loading : TitleState()
object Locked : TitleState()
data object Locked : TitleState()
}
@Immutable
sealed class SubtitleState {
data class CryptoPriceContent(
val price: String,
val priceChangePercent: String,
val type: PriceChangeType,
) : SubtitleState()
data class TextContent(val value: String) : SubtitleState()
data object Unknown : SubtitleState()
data object Loading : SubtitleState()
data object Locked : SubtitleState()
}
@Immutable
@ -134,34 +175,19 @@ internal sealed class TokenItemState {
val hasStaked: Boolean = false,
) : FiatAmountState()
object Loading : FiatAmountState()
data object Loading : FiatAmountState()
object Locked : FiatAmountState()
data object Locked : FiatAmountState()
}
@Immutable
sealed class CryptoAmountState {
data class Content(val text: String) : CryptoAmountState()
object Unreachable : CryptoAmountState()
data object Unreachable : CryptoAmountState()
object Loading : CryptoAmountState()
data object Loading : CryptoAmountState()
object Locked : CryptoAmountState()
}
sealed class CryptoPriceState {
data class Content(
val price: String,
val priceChangePercent: String?,
val type: PriceChangeType?,
) : CryptoPriceState()
object Unknown : CryptoPriceState()
object Loading : CryptoPriceState()
object Locked : CryptoPriceState()
data object Locked : CryptoAmountState()
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.core.ui.decompose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@Stable
interface ComposableBottomSheetComponent {
fun dismiss()
@Composable
fun BottomSheet()
}

View file

@ -6,6 +6,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import com.tangem.core.ui.windowsize.rememberWindowSizePreview
@Composable
@ -14,12 +16,14 @@ fun TangemThemePreview(
typography: TangemTypography = TangemTheme.typography,
dimens: TangemDimens = TangemTheme.dimens,
alwaysShowBottomSheets: Boolean = true,
rtl: Boolean = false,
content: @Composable () -> Unit,
) {
val isDarkTheme = isDark ?: isSystemInDarkTheme()
CompositionLocalProvider(
LocalBottomSheetAlwaysVisible provides alwaysShowBottomSheets,
LocalLayoutDirection provides if (rtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
) {
BoxWithConstraints {
TangemTheme(

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="#1E1E1E"
android:pathData="M8.82,17.87C9.04,16.02 9.74,14.31 10.8,12.89C10.41,13.08 9.93,13.2 9.38,13.24V14H8.55V13.25C6.83,13.13 5.74,12.22 5.74,10.86H7.02C7.08,11.56 7.66,12.04 8.55,12.14V9.48L8.02,9.35C6.67,9.02 5.94,8.23 5.94,7.09C5.94,5.75 6.93,4.85 8.55,4.71V3.9H9.38V4.71C10.94,4.84 11.97,5.77 12,7.06H10.74C10.72,6.42 10.18,5.92 9.38,5.83V8.34L9.94,8.47C11.45,8.82 12.16,9.56 12.16,10.77C12.16,11.03 12.13,11.27 12.07,11.49C13.63,10.05 15.64,9.08 17.87,8.82C17.81,3.94 13.83,0 8.94,0C4,0 0,4 0,8.94C0,13.83 3.94,17.81 8.82,17.87ZM17.78,10.22C13.87,10.79 10.79,13.87 10.22,17.78L17.78,10.22ZM8.55,8.17C7.69,8.01 7.23,7.59 7.23,6.99C7.23,6.35 7.78,5.86 8.55,5.82V8.17ZM9.38,12.15V9.63C10.4,9.83 10.88,10.24 10.88,10.91C10.88,11.65 10.33,12.1 9.38,12.15Z" />
<path
android:fillColor="#1E1E1E"
android:pathData="M10.13,19.07C10.13,14.13 14.13,10.13 19.06,10.13C24,10.13 28,14.13 28,19.07C28,24 24,28 19.06,28C14.13,28 10.13,24 10.13,19.07ZM19.53,17.53L20.48,14.82L19.07,14.32L17.78,17.96L15.34,18.57L15.7,20.03L17.18,19.66L16.23,22.36C16.15,22.59 16.18,22.84 16.33,23.04C16.47,23.24 16.69,23.36 16.94,23.36H22.61V21.86H18L18.93,19.22L21.37,18.61L21.01,17.16L19.53,17.53Z" />
</vector>

View file

@ -1,17 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<group>
<clip-path android:pathData="M0,0h28v28h-28z" />
<path
android:pathData="M8.821,17.869C9.035,16.016 9.741,14.312 10.803,12.892C10.407,13.076 9.929,13.195 9.38,13.241V14H8.552V13.247C6.83,13.134 5.744,12.22 5.738,10.863H7.015C7.077,11.556 7.657,12.04 8.552,12.142V9.483L8.015,9.346C6.67,9.017 5.935,8.234 5.935,7.093C5.935,5.749 6.929,4.852 8.552,4.709V3.902H9.38V4.709C10.941,4.84 11.966,5.766 12.003,7.063H10.744C10.719,6.424 10.176,5.922 9.38,5.826V8.342L9.941,8.467C11.454,8.82 12.163,9.561 12.163,10.768C12.163,11.028 12.13,11.27 12.065,11.493C13.628,10.047 15.64,9.079 17.869,8.822C17.808,3.939 13.831,0 8.935,0C4,0 0,4 0,8.935C0,13.831 3.939,17.808 8.821,17.869ZM17.776,10.222C13.873,10.786 10.786,13.873 10.222,17.776L17.776,10.222ZM8.552,8.174C7.688,8.007 7.225,7.589 7.225,6.991C7.225,6.352 7.781,5.862 8.552,5.82V8.174ZM9.38,12.154V9.632C10.404,9.83 10.88,10.236 10.88,10.905C10.88,11.646 10.33,12.1 9.38,12.154Z"
android:fillColor="#000000"
android:fillType="evenOdd" />
<path
android:pathData="M10.13,19.066C10.13,14.132 14.13,10.131 19.064,10.131C23.998,10.131 27.999,14.132 27.999,19.066C27.999,24 23.998,28.001 19.064,28.001C14.13,28.001 10.13,24 10.13,19.066ZM19.527,17.526L20.48,14.823L19.066,14.323L17.782,17.962L15.337,18.573L15.701,20.028L17.183,19.658L16.23,22.36C16.149,22.589 16.184,22.844 16.325,23.043C16.465,23.241 16.694,23.36 16.937,23.36H22.61V21.86H17.997L18.928,19.222L21.374,18.61L21.01,17.155L19.527,17.526Z"
android:fillColor="#000000"
android:fillType="evenOdd" />
</group>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="36dp"
android:viewportWidth="22"
android:viewportHeight="36">
<path
android:fillColor="#C9C9CA"
android:pathData="M22,2L22,34A2,2 0,0 1,20 36L2,36A2,2 0,0 1,0 34L0,2A2,2 0,0 1,2 0L20,0A2,2 0,0 1,22 2z" />
<path
android:fillColor="#A1A1A1"
android:pathData="M10.92,22.14H10.96C11.52,22.14 11.85,21.83 11.85,21.35C11.85,20.84 11.48,20.55 10.95,20.55H10.91C10.36,20.55 10.03,20.89 10.03,21.32C10.03,21.79 10.36,22.14 10.92,22.14ZM9.29,19.89V20.58C9,20.63 8.83,20.85 8.83,21.3C8.83,21.82 9.09,22.11 9.62,22.11H10.05C9.77,21.95 9.51,21.59 9.51,21.16C9.51,20.42 10.06,19.86 10.9,19.86H10.94C11.76,19.86 12.38,20.42 12.38,21.17C12.38,21.65 12.16,21.95 11.87,22.11H12.32V22.79H9.61C8.75,22.78 8.32,22.21 8.32,21.3C8.32,20.39 8.73,19.97 9.29,19.89ZM11.17,7.82V9.23H9.41C9.06,9.23 8.88,9.23 8.75,9.16C8.63,9.1 8.54,9 8.48,8.89C8.41,8.75 8.41,8.58 8.41,8.23V7.82H11.17ZM13.69,8.23V6C13.69,5.65 13.69,5.48 13.62,5.34C13.56,5.22 13.47,5.13 13.35,5.07C13.21,5 13.04,5 12.69,5H12.31V9.23H12.69H12.69C13.04,9.23 13.21,9.23 13.35,9.16C13.47,9.1 13.56,9 13.62,8.89C13.69,8.75 13.69,8.58 13.69,8.23ZM11.17,5V6.41H8.41V6C8.41,5.65 8.41,5.48 8.48,5.34C8.54,5.22 8.63,5.13 8.75,5.07C8.88,5 9.06,5 9.41,5L11.17,5ZM10.21,11.96H11.81V11.57H12.32V11.96H12.96L12.96,12.64H12.32V13.28H11.81V12.64H10.27C10.01,12.64 9.89,12.76 9.89,12.98C9.89,13.11 9.91,13.21 9.95,13.31H9.41C9.37,13.2 9.34,13.05 9.34,12.85C9.34,12.27 9.65,11.96 10.21,11.96ZM12.32,17.44V16.76H9.39V17.44H11.11C11.58,17.44 11.81,17.74 11.81,18.12C11.81,18.53 11.61,18.71 11.17,18.71H9.39L9.39,19.38H11.23C12.04,19.38 12.38,18.97 12.38,18.38C12.38,17.9 12.14,17.58 11.85,17.44H12.32ZM11.88,24.74C11.88,25.16 11.66,25.42 11.16,25.45V23.99C11.61,24.06 11.88,24.33 11.88,24.74ZM10.87,23.29H10.82C9.9,23.29 9.33,23.91 9.33,24.77C9.33,25.52 9.67,26.02 10.29,26.11V25.46C10,25.41 9.84,25.19 9.84,24.79C9.84,24.28 10.15,24 10.7,23.98V26.12H10.9C11.95,26.12 12.38,25.47 12.38,24.74C12.38,23.91 11.77,23.29 10.87,23.29ZM12.32,26.64V27.32H11.87C12.14,27.46 12.38,27.78 12.38,28.21C12.38,28.59 12.22,28.89 11.85,29.04C12.22,29.26 12.38,29.66 12.38,30.03C12.38,30.56 12.05,31 11.24,31H9.39V30.32H11.2C11.63,30.32 11.81,30.14 11.81,29.8C11.81,29.47 11.59,29.16 11.14,29.16H9.39V28.48H11.2C11.63,28.48 11.81,28.29 11.81,27.96C11.81,27.63 11.59,27.32 11.14,27.32H9.39V26.64H12.32ZM10.7,15.52H10.41C10.04,15.52 9.82,15.22 9.82,14.8C9.82,14.47 9.98,14.33 10.23,14.33C10.59,14.33 10.7,14.66 10.7,15.18V15.52ZM11.13,15.16C11.13,14.32 10.88,13.66 10.2,13.66C9.59,13.66 9.33,14.1 9.33,14.64C9.33,15.09 9.5,15.34 9.75,15.53H9.39V16.2H11.31C12.11,16.2 12.38,15.68 12.38,15.03C12.38,14.38 12.09,13.83 11.41,13.78V14.43C11.7,14.47 11.87,14.64 11.87,14.99C11.87,15.39 11.67,15.52 11.28,15.52H11.13V15.16Z" />
</vector>

View file

@ -1,6 +1,7 @@
package com.tangem.data.feedback.converters
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.util.getBackupCardsCount
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
@ -20,10 +21,7 @@ internal object CardInfoConverter : Converter<ScanResponse, CardInfo> {
CardInfo(
userWalletId = createUserWalletId(scanResponse = value),
cardId = card.cardId,
cardsCount = when (val status = value.card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount.toString()
else -> "0"
},
cardsCount = value.getBackupCardsCount()?.toString() ?: "0",
firmwareVersion = card.firmwareVersion.stringValue,
cardBlockchain = walletData?.blockchain,
signedHashesList = card.wallets.map {

View file

@ -108,6 +108,22 @@ internal class DefaultMarketsTokenRepository(
return TokenChartConverter.convert(interval, response.getOrThrow())
}
override suspend fun getChartPreview(
fiatCurrencyCode: String,
interval: PriceChangeInterval,
tokenId: String,
): TokenChart {
val response = marketsApi.getCoinsListCharts(
coinIds = tokenId,
currency = fiatCurrencyCode,
interval = interval.toRequestParam(),
)
val chart = response.getOrThrow()[tokenId] ?: error("No chart preview data for token $tokenId")
return TokenChartConverter.convert(interval, chart)
}
override suspend fun getTokenInfo(
fiatCurrencyCode: String,
tokenId: String,

View file

@ -2,10 +2,13 @@ package com.tangem.data.staking
import android.util.Base64
import arrow.core.raise.catch
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toCompressedPublicKey
@ -22,6 +25,7 @@ import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.stakekit.models.request.*
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectListSync
@ -45,7 +49,6 @@ import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
@ -67,6 +70,7 @@ internal class DefaultStakingRepository(
private val dispatchers: CoroutineDispatcherProvider,
private val stakingFeatureToggle: StakingFeatureToggles,
private val walletManagersFacade: WalletManagersFacade,
moshi: Moshi,
) : StakingRepository {
private val stakingNetworkTypeConverter = StakingNetworkTypeConverter()
@ -104,6 +108,13 @@ internal class DefaultStakingRepository(
value = emptyMap<UserWalletId, Boolean>(),
)
private val tronStakeKitTransactionAdapter: JsonAdapter<TronStakeKitTransaction> =
moshi.adapter(TronStakeKitTransaction::class.java)
override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) {
rawNetworkId.plus(rawCurrencyId)
}
override fun isStakingSupported(integrationKey: String): Boolean {
return integrationIdMap.containsKey(integrationKey)
}
@ -141,8 +152,8 @@ internal class DefaultStakingRepository(
val yield = getYield(cryptoCurrencyId, symbol)
StakingEntryInfo(
interestRate = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr),
periodInDays = yield.metadata.cooldownPeriod.days,
apr = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr),
rewardSchedule = yield.metadata.rewardSchedule,
tokenSymbol = yield.token.symbol,
)
}
@ -158,7 +169,7 @@ internal class DefaultStakingRepository(
val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable
val prefetchedYield = findPrefetchedYield(yields, rawCurrencyId, symbol)
val isSupported = isStakingSupported(cryptoCurrencyId.getIntegrationKey())
val isSupported = isStakingSupported(getIntegrationKey(cryptoCurrencyId))
when {
prefetchedYield != null && isSupported -> {
@ -261,25 +272,27 @@ internal class DefaultStakingRepository(
override suspend fun fetchSingleYieldBalance(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
cryptoCurrency: CryptoCurrency,
refresh: Boolean,
) = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
val cryptoCurrency = address.cryptoCurrency
val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] ?: return@withContext
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: return@withContext
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty()
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
block = {
val requestBody = getBalanceRequestData(address.address, integrationId)
val requestBody = getBalanceRequestData(address, integrationId)
val result = stakeKitApi.getSingleYieldBalance(
integrationId = requestBody.integrationId,
body = requestBody,
).getOrThrow()
stakingBalanceStore.store(
userWalletId,
requestBody.integrationId,
YieldBalanceWrapperDTO(
balances = result,
@ -292,15 +305,15 @@ internal class DefaultStakingRepository(
override fun getSingleYieldBalanceFlow(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
cryptoCurrency: CryptoCurrency,
): Flow<YieldBalance> = channelFlow {
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalance.Empty)
} else {
launch(dispatchers.io) {
val integrationId = integrationIdMap[address.cryptoCurrency.id.getIntegrationKey()]
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
stakingBalanceStore.get(integrationId)
stakingBalanceStore.get(userWalletId, integrationId)
.collectLatest {
send(
yieldBalanceConverter.convert(
@ -316,7 +329,7 @@ internal class DefaultStakingRepository(
withContext(dispatchers.io) {
fetchSingleYieldBalance(
userWalletId,
address,
cryptoCurrency,
)
}
}
@ -324,16 +337,18 @@ internal class DefaultStakingRepository(
override suspend fun getSingleYieldBalanceSync(
userWalletId: UserWalletId,
address: CryptoCurrencyAddress,
cryptoCurrency: CryptoCurrency,
): YieldBalance = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) {
YieldBalance.Empty
} else {
fetchSingleYieldBalance(userWalletId, address)
fetchSingleYieldBalance(userWalletId, cryptoCurrency)
val integrationId = integrationIdMap[address.cryptoCurrency.id.getIntegrationKey()]
val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
?: error("Could not get integrationId")
val result = stakingBalanceStore.getSyncOrNull(integrationId) ?: return@withContext YieldBalance.Error
val result = stakingBalanceStore.getSyncOrNull(userWalletId, integrationId)
?: return@withContext YieldBalance.Error
yieldBalanceConverter.convert(
YieldBalanceConverter.Data(
balance = result,
@ -345,7 +360,7 @@ internal class DefaultStakingRepository(
override suspend fun fetchMultiYieldBalance(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
refresh: Boolean,
) = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
@ -357,23 +372,23 @@ internal class DefaultStakingRepository(
key = getYieldBalancesKey(userWalletId),
skipCache = refresh,
block = {
val result = stakeKitApi.getMultipleYieldBalances(
addresses
.mapNotNull { networkAddress ->
val cryptoCurrency = networkAddress.cryptoCurrency
val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()]
val availableCurrencies = cryptoCurrencies
.mapNotNull { currency ->
val address = walletManagersFacade.getDefaultAddress(userWalletId, currency.network)
val integrationId = integrationIdMap[getIntegrationKey(currency.id)]
if (integrationId != null) {
networkAddress.address to integrationId
} else {
null
}
if (integrationId != null && address != null) {
address to integrationId
} else {
null
}
.distinct()
.map { getBalanceRequestData(it.first, it.second) },
).getOrThrow()
}
.distinct()
.map { getBalanceRequestData(it.first, it.second) }
.ifEmpty { return@invokeOnExpire }
val result = stakeKitApi.getMultipleYieldBalances(availableCurrencies).getOrThrow()
stakingBalanceStore.store(result)
stakingBalanceStore.store(userWalletId, result)
},
)
} finally {
@ -385,20 +400,20 @@ internal class DefaultStakingRepository(
override fun getMultiYieldBalanceFlow(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
): Flow<YieldBalanceList> = channelFlow {
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalanceList.Empty)
} else {
launch(dispatchers.io) {
stakingBalanceStore.get()
stakingBalanceStore.get(userWalletId)
.collectLatest { send(yieldBalanceListConverter.convert(it)) }
}
withContext(dispatchers.io) {
fetchMultiYieldBalance(
userWalletId,
addresses,
cryptoCurrencies,
)
}
}
@ -406,14 +421,14 @@ internal class DefaultStakingRepository(
override fun getMultiYieldBalanceLce(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
): LceFlow<Throwable, YieldBalanceList> = lceFlow {
if (!stakingFeatureToggle.isStakingEnabled) {
send(YieldBalanceList.Empty)
} else {
launch(dispatchers.io) {
combine(
stakingBalanceStore.get(),
stakingBalanceStore.get(userWalletId),
isYieldBalanceFetching.map { it.getOrElse(userWalletId) { false } },
) { result, isFetching ->
val balances = yieldBalanceListConverter.convert(result)
@ -422,7 +437,7 @@ internal class DefaultStakingRepository(
}
withContext(dispatchers.io) {
catch(
block = { fetchMultiYieldBalance(userWalletId, addresses, refresh = false) },
block = { fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) },
catch = { raise(it) },
)
}
@ -431,13 +446,13 @@ internal class DefaultStakingRepository(
override suspend fun getMultiYieldBalanceSync(
userWalletId: UserWalletId,
addresses: List<CryptoCurrencyAddress>,
cryptoCurrencies: List<CryptoCurrency>,
): YieldBalanceList = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) {
YieldBalanceList.Empty
} else {
fetchMultiYieldBalance(userWalletId, addresses)
val result = stakingBalanceStore.getSyncOrNull() ?: return@withContext YieldBalanceList.Error
fetchMultiYieldBalance(userWalletId, cryptoCurrencies)
val result = stakingBalanceStore.getSyncOrNull(userWalletId) ?: return@withContext YieldBalanceList.Error
yieldBalanceListConverter.convert(result)
}
}
@ -506,6 +521,8 @@ internal class DefaultStakingRepository(
amount = params.amount.toPlainString(),
inputToken = tokenConverter.convertBack(params.token),
validatorAddress = params.validatorAddress,
validatorAddresses = listOf(params.validatorAddress), // check on other networks
tronResource = getTronResource(network),
),
)
}
@ -539,16 +556,8 @@ internal class DefaultStakingRepository(
}
}
override fun isStakeMoreAvailable(networkId: Network.ID): Boolean {
val blockchain = Blockchain.fromId(networkId.value)
return when (blockchain) {
Blockchain.Solana -> false
else -> true
}
}
override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval {
return when (cryptoCurrency.id.getIntegrationKey()) {
return when (getIntegrationKey(cryptoCurrency.id)) {
Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() -> {
StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER)
}
@ -562,8 +571,14 @@ internal class DefaultStakingRepository(
Blockchain.Solana,
Blockchain.Cosmos,
-> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes())
Blockchain.BSC,
Blockchain.Ethereum,
-> TransactionData.Compiled.Data.RawString(unsignedTransaction)
Blockchain.Tron -> {
val tronStakeKitTransaction = tronStakeKitTransactionAdapter.fromJson(unsignedTransaction)
?: error("Failed to parse Tron StakeKit transaction")
TransactionData.Compiled.Data.RawString(tronStakeKitTransaction.rawDataHex)
}
else -> error("Unsupported blockchain")
}
}
@ -593,7 +608,15 @@ internal class DefaultStakingRepository(
private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}"
private fun CryptoCurrency.ID.getIntegrationKey(): String = rawNetworkId.plus(rawCurrencyId)
private fun getTronResource(network: Network): TronResource? {
val blockchain = Blockchain.fromNetworkId(network.backendId)
return if (blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet) {
TronResource.ENERGY
} else {
null
}
}
private companion object {
const val YIELDS_STORE_KEY = "yields"
@ -601,11 +624,11 @@ internal class DefaultStakingRepository(
const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking"
const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking"
const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking"
const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking"
const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking"
const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking"
const val TRON_INTEGRATION_ID = "tron-trx-native-staking"
const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking"
const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking"
const val KAVA_INTEGRATION_ID = "kava-kava-native-staking"
const val NEAR_INTEGRATION_ID = "near-near-native-staking"
const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking"
@ -617,11 +640,11 @@ internal class DefaultStakingRepository(
Blockchain.Solana.run { id + toCoinId() } to SOLANA_INTEGRATION_ID,
Blockchain.Cosmos.run { id + toCoinId() } to COSMOS_INTEGRATION_ID,
Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID,
Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID,
// Blockchain.Polkadot.run { id + toCoinId() } to POLKADOT_INTEGRATION_ID,
// Blockchain.Avalanche.run { id + toCoinId() } to AVALANCHE_INTEGRATION_ID,
// Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID,
Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID,
// Blockchain.Cronos.run { id + toCoinId() } to CRONOS_INTEGRATION_ID,
// Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID,
// Blockchain.Kava.run { id + toCoinId() } to KAVA_INTEGRATION_ID,
// Blockchain.Near.run { id + toCoinId() } to NEAR_INTEGRATION_ID,
// Blockchain.Tezos.run { id + toCoinId() } to TEZOS_INTEGRATION_ID,

View file

@ -26,6 +26,7 @@ internal class YieldBalanceConverter : Converter<YieldBalanceConverter.Data, Yie
rawCurrencyId = item.tokenDTO.coinGeckoId,
rawNetworkId = item.tokenDTO.network.name,
validatorAddress = item.validatorAddress,
date = item.date?.toDateTime(),
pendingActions = pendingActionConverter.convertList(item.pendingActions),
)
},

View file

@ -4,13 +4,13 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap
import com.tangem.domain.staking.model.stakekit.YieldBalanceList
import com.tangem.utils.converter.Converter
internal class YieldBalanceListConverter : Converter<List<YieldBalanceWrapperDTO>, YieldBalanceList> {
internal class YieldBalanceListConverter : Converter<Set<YieldBalanceWrapperDTO>, YieldBalanceList> {
internal val converter by lazy(LazyThreadSafetyMode.NONE) {
YieldBalanceConverter()
}
override fun convert(value: List<YieldBalanceWrapperDTO>): YieldBalanceList {
override fun convert(value: Set<YieldBalanceWrapperDTO>): YieldBalanceList {
return if (value.isEmpty()) {
YieldBalanceList.Empty
} else {

View file

@ -37,6 +37,7 @@ internal object StakingDataModule {
stakingFeatureToggle: StakingFeatureToggles,
cacheRegistry: CacheRegistry,
walletManagersFacade: WalletManagersFacade,
@NetworkMoshi moshi: Moshi,
): StakingRepository {
return DefaultStakingRepository(
stakeKitApi = stakeKitApi,
@ -47,6 +48,7 @@ internal object StakingDataModule {
cacheRegistry = cacheRegistry,
stakingFeatureToggle = stakingFeatureToggle,
walletManagersFacade = walletManagersFacade,
moshi = moshi,
)
}

View file

@ -8,7 +8,6 @@ import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -28,7 +27,7 @@ internal object TokensDataModule {
fun provideCurrenciesRepository(
tangemTechApi: TangemTechApi,
tangemExpressApi: TangemExpressApi,
userTokensStore: UserTokensStore,
appPreferencesStore: AppPreferencesStore,
userWalletsStore: UserWalletsStore,
walletManagersFacade: WalletManagersFacade,
expressAssetsStore: ExpressAssetsStore,
@ -38,7 +37,7 @@ internal object TokensDataModule {
return DefaultCurrenciesRepository(
tangemTechApi = tangemTechApi,
tangemExpressApi = tangemExpressApi,
userTokensStore = userTokensStore,
appPreferencesStore = appPreferencesStore,
walletManagersFacade = walletManagersFacade,
userWalletsStore = userWalletsStore,
expressAssetsStore = expressAssetsStore,
@ -71,7 +70,7 @@ internal object TokensDataModule {
networksStatusesStore: NetworksStatusesStore,
walletManagersFacade: WalletManagersFacade,
userWalletsStore: UserWalletsStore,
userTokensStore: UserTokensStore,
appPreferencesStore: AppPreferencesStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
): NetworksRepository {
@ -79,7 +78,7 @@ internal object TokensDataModule {
networksStatusesStore = networksStatusesStore,
walletManagersFacade = walletManagersFacade,
userWalletsStore = userWalletsStore,
userTokensStore = userTokensStore,
appPreferencesStore = appPreferencesStore,
cacheRegistry = cacheRegistry,
dispatchers = dispatchers,
)

View file

@ -18,8 +18,12 @@ import com.tangem.datasource.api.express.models.request.AssetsRequestBody
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.datasource.local.token.ExpressAssetsStore
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
@ -46,11 +50,11 @@ import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency
internal class DefaultCurrenciesRepository(
private val tangemTechApi: TangemTechApi,
private val tangemExpressApi: TangemExpressApi,
private val userTokensStore: UserTokensStore,
private val userWalletsStore: UserWalletsStore,
private val walletManagersFacade: WalletManagersFacade,
private val expressAssetsStore: ExpressAssetsStore,
private val cacheRegistry: CacheRegistry,
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : CurrenciesRepository {
@ -86,7 +90,7 @@ internal class DefaultCurrenciesRepository(
override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
return withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = userTokensStore.getSyncOrNull(userWalletId),
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action" },
)
@ -141,7 +145,7 @@ internal class DefaultCurrenciesRepository(
override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) =
withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = userTokensStore.getSyncOrNull(userWalletId),
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform remove currency action" },
)
@ -163,7 +167,7 @@ internal class DefaultCurrenciesRepository(
override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
return withContext(dispatchers.io) {
val savedCurrencies = requireNotNull(
value = userTokensStore.getSyncOrNull(userWalletId),
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform remove currencies action" },
)
@ -276,13 +280,31 @@ internal class DefaultCurrenciesRepository(
fetchTokensIfCacheExpired(userWallet, refresh)
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
val storedTokens = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWallet.walletId),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.scanResponse)
}
override suspend fun getMultiCurrencyWalletCachedCurrenciesSync(userWalletId: UserWalletId) =
withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
val storedTokens = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWallet.walletId),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.scanResponse)
}
override suspend fun getMultiCurrencyWalletCurrency(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,
@ -290,9 +312,12 @@ internal class DefaultCurrenciesRepository(
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
val response = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
responseCurrenciesFactory.createCurrency(
currencyId = id,
@ -312,9 +337,12 @@ internal class DefaultCurrenciesRepository(
fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false)
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
val storedTokens = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
val blockchain = Blockchain.fromId(networkId.value)
val blockchainNetworkId = blockchain.toNetworkId()
val coinId = blockchain.toCoinId()
@ -335,7 +363,7 @@ internal class DefaultCurrenciesRepository(
ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
userTokensStore.get(userWalletId)
getSavedUserTokensResponse(userWalletId)
.map { it.group == UserTokensResponse.GroupType.NETWORK }
.collect(::send)
}
@ -347,7 +375,7 @@ internal class DefaultCurrenciesRepository(
ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true)
launch(dispatchers.io) {
userTokensStore.get(userWalletId)
getSavedUserTokensResponse(userWalletId)
.map { it.sort == UserTokensResponse.SortType.BALANCE }
.collect(::send)
}
@ -461,9 +489,14 @@ internal class DefaultCurrenciesRepository(
val userWallet = getUserWallet(userWalletId)
fetchTokensIfCacheExpired(userWallet, refresh = false)
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
val storedTokens = requireNotNull(
value = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue),
),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
return storedTokens.tokens.any {
it.contractAddress != null &&
@ -473,7 +506,7 @@ internal class DefaultCurrenciesRepository(
}
private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow<List<CryptoCurrency>> {
return userTokensStore.get(userWallet.walletId).map { storedTokens ->
return getSavedUserTokensResponse(userWallet.walletId).map { storedTokens ->
responseCurrenciesFactory.createCurrencies(
response = storedTokens,
scanResponse = userWallet.scanResponse,
@ -517,17 +550,26 @@ internal class DefaultCurrenciesRepository(
.let { customTokensMerger.mergeIfPresented(userWalletId, response) }
.let(userTokensBackwardCompatibility::applyCompatibilityAndGetUpdated)
userTokensStore.store(userWallet.walletId, compatibleUserTokensResponse)
appPreferencesStore.storeObject(
key = PreferencesKeys.getUserTokensKey(userWalletId = userWallet.walletId.stringValue),
value = compatibleUserTokensResponse,
)
fetchExchangeableUserMarketCoinsByIds(userWalletId, compatibleUserTokensResponse)
}
private suspend fun checkIsEmptyDemoWallet(userWallet: UserWallet): Boolean {
return demoConfig.isDemoCardId(userWallet.cardId) && userTokensStore.getSyncOrNull(userWallet.walletId) == null
val response = getSavedUserTokensResponseSync(key = userWallet.walletId)
return demoConfig.isDemoCardId(userWallet.cardId) && response == null
}
private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) {
val compatibleUserTokensResponse = userTokensBackwardCompatibility.applyCompatibilityAndGetUpdated(response)
userTokensStore.store(userWalletId, compatibleUserTokensResponse)
appPreferencesStore.storeObject(
key = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId.stringValue),
value = compatibleUserTokensResponse,
)
pushTokens(userWalletId, response)
}
@ -561,8 +603,9 @@ internal class DefaultCurrenciesRepository(
private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse {
val userWalletId = userWallet.walletId
val response = userTokensStore.getSyncOrNull(userWalletId)
?: createDefaultUserTokensResponse(userWallet)
val response = appPreferencesStore.getObjectSyncOrNull(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
) ?: createDefaultUserTokensResponse(userWallet)
if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) {
Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId")
@ -622,4 +665,16 @@ internal class DefaultCurrenciesRepository(
}
private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}"
private fun getSavedUserTokensResponse(key: UserWalletId): Flow<UserTokensResponse> {
return appPreferencesStore
.getObject<UserTokensResponse>(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue))
.filterNotNull()
}
private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? {
return appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(key.stringValue),
)
}
}

View file

@ -8,8 +8,11 @@ import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory
import com.tangem.data.tokens.utils.NetworkStatusFactory
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.core.lce.LceFlow
@ -33,7 +36,7 @@ internal class DefaultNetworksRepository(
private val networksStatusesStore: NetworksStatusesStore,
private val walletManagersFacade: WalletManagersFacade,
private val userWalletsStore: UserWalletsStore,
private val userTokensStore: UserTokensStore,
private val appPreferencesStore: AppPreferencesStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
) : NetworksRepository {
@ -300,9 +303,14 @@ internal class DefaultNetworksRepository(
}
return if (userWallet.isMultiCurrency) {
val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
val response = requireNotNull(
value = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
key = PreferencesKeys.getUserTokensKey(userWalletId.stringValue),
),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse).asSequence()
} else {

View file

@ -86,6 +86,12 @@ internal class FeedbackDataBuilder {
builder.appendKeyValue("Fee", error.fee ?: "Unable to receive")
}
fun addStakingInfo(validatorName: String?, transactionType: String?, unsignedTransaction: String?) {
builder.appendKeyValue("Validator", validatorName ?: "unknown")
builder.appendKeyValue("Action", transactionType ?: "unknown")
builder.appendKeyValue("Unsigned transaction", unsignedTransaction ?: "unknown")
}
fun addDelimiter(): StringBuilder = builder.appendDelimiter()
fun build(): String = builder.trimEnd().toString()

View file

@ -14,7 +14,7 @@ class SaveBlockchainErrorUseCase(
private val feedbackRepository: FeedbackRepository,
) {
fun invoke(error: BlockchainErrorInfo) {
operator fun invoke(error: BlockchainErrorInfo) {
feedbackRepository.saveBlockchainErrorInfo(error = error)
}
}

View file

@ -22,4 +22,12 @@ sealed interface FeedbackEmailType {
/** User has problem with sending transaction */
data class TransactionSendingProblem(override val cardInfo: CardInfo) : FeedbackEmailType
/** User has problem with staking */
data class StakingProblem(
override val cardInfo: CardInfo,
val validatorName: String?,
val transactionType: String?,
val unsignedTransaction: String?,
) : FeedbackEmailType
}

View file

@ -23,6 +23,12 @@ internal class EmailMessageBodyResolver(
is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.cardInfo)
is FeedbackEmailType.ScanningProblem -> addScanningProblemBody()
is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.cardInfo)
is FeedbackEmailType.StakingProblem -> addStakingProblemBody(
type.cardInfo,
type.validatorName,
type.transactionType,
type.unsignedTransaction,
)
}
return build()
@ -72,6 +78,36 @@ internal class EmailMessageBodyResolver(
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}
private suspend fun FeedbackDataBuilder.addStakingProblemBody(
cardInfo: CardInfo,
validatorName: String?,
transactionType: String?,
unsignedTransaction: String?,
) {
addCardInfo(cardInfo)
addDelimiter()
val userWalletId = requireNotNull(cardInfo.userWalletId) { "UserWalletId must be not null" }
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
val blockchainInfo = blockchainError?.let {
feedbackRepository.getBlockchainInfo(
userWalletId = userWalletId,
blockchainId = blockchainError.blockchainId,
derivationPath = blockchainError.derivationPath,
)
}
if (blockchainInfo != null) {
addBlockchainError(blockchainInfo, blockchainError)
addDelimiter()
}
addStakingInfo(validatorName, transactionType, unsignedTransaction)
addDelimiter()
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}
private fun FeedbackDataBuilder.addCardAndPhoneInfo(cardInfo: CardInfo) {
addCardInfo(cardInfo)
addDelimiter()

View file

@ -19,7 +19,9 @@ internal class EmailMessageTitleResolver(private val resources: Resources) {
is FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support
is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative
is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed
is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_preface_tx_failed
is FeedbackEmailType.TransactionSendingProblem,
is FeedbackEmailType.StakingProblem,
-> R.string.feedback_preface_tx_failed
}
.let(resources::getString)
}

View file

@ -26,6 +26,7 @@ internal class EmailSubjectResolver(private val resources: Resources) {
is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative
is FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed
is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed
is FeedbackEmailType.StakingProblem -> R.string.feedback_subject_tx_failed
}
.let(resources::getString)
}

View file

@ -10,54 +10,8 @@ interface CardTypesResolver {
fun isTangemWallet(): Boolean
fun isWhiteWallet2(): Boolean
fun isAvroraWallet(): Boolean
fun isTraillantWallet(): Boolean
fun isShibaWallet(): Boolean
fun isTronWallet(): Boolean
fun isKaspaWallet(): Boolean
fun isKaspa2Wallet(): Boolean
fun isKaspaResellerWallet(): Boolean
fun isBadWallet(): Boolean
fun isJrWallet(): Boolean
fun isGrimWallet(): Boolean
fun isSatoshiFriendsWallet(): Boolean
fun isBitcoinPizzaDayWallet(): Boolean
fun isVeChainWallet(): Boolean
fun isNewWorldEliteWallet(): Boolean
fun isRedPandaWallet(): Boolean
fun isCryptoSethWallet(): Boolean
fun isKishuInuWallet(): Boolean
fun isBabyDogeWallet(): Boolean
fun isCOQWallet(): Boolean
fun isCoinMetricaWallet(): Boolean
fun isVoltInuWallet(): Boolean
fun isVividWallet(): Boolean
fun isPastelWallet(): Boolean
fun isWhiteWallet(): Boolean
fun isWallet2(): Boolean

View file

@ -26,60 +26,10 @@ internal class TangemCardTypesResolver(
card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable
}
override fun isWhiteWallet2(): Boolean = card.batchId == WHITE_WALLET2_BATCH_ID
override fun isAvroraWallet(): Boolean = card.batchId == AVRORA_WALLET_BATCH_ID
override fun isTraillantWallet(): Boolean = card.batchId == TRILLIANT_WALLET_BATCH_ID
override fun isShibaWallet(): Boolean {
return card.firmwareVersion.compareTo(FirmwareVersion.KeysImportAvailable) == 0
}
override fun isTronWallet(): Boolean = card.batchId == TRON_WALLET_BATCH_ID
override fun isKaspaWallet(): Boolean = card.batchId == KASPA_WALLET_BATCH_ID
override fun isKaspa2Wallet(): Boolean = card.batchId == KASPA2_WALLET_BATCH_ID
override fun isKaspaResellerWallet(): Boolean = card.batchId == KASPA_RESELLER_WALLET_BATCH_ID
override fun isBadWallet(): Boolean = card.batchId == BAD_WALLET_BATCH_ID
override fun isJrWallet(): Boolean = card.batchId == JR_WALLET_BATCH_ID
override fun isGrimWallet(): Boolean = card.batchId == GRIM_WALLET_BATCH_ID
override fun isSatoshiFriendsWallet(): Boolean = card.batchId == SATOSHI_WALLET_BATCH_ID
override fun isBitcoinPizzaDayWallet(): Boolean = card.batchId == BITCOIN_PIZZA_DAY_WALLET_BATCH_ID
override fun isVeChainWallet(): Boolean = card.batchId == VECHAIN_WALLET_BATCH_ID
override fun isNewWorldEliteWallet(): Boolean = card.batchId == NEW_WORLD_ELITE_WALLET_BATCH_ID
override fun isRedPandaWallet(): Boolean = card.batchId == RED_PANDA_WALLET_BATCH_ID
override fun isCryptoSethWallet(): Boolean = card.batchId == CRYPTO_SETH_WALLET_BATCH_ID
override fun isKishuInuWallet(): Boolean = card.batchId == KISHU_INU_WALLET_BATCH_ID
override fun isBabyDogeWallet(): Boolean = card.batchId == BABY_DOGE_WALLET_BATCH_ID
override fun isCOQWallet(): Boolean = card.batchId == COQ_WALLET_BATCH_ID
override fun isCoinMetricaWallet(): Boolean = card.batchId == COIN_METRICA_WALLET_BATCH_ID
override fun isVoltInuWallet(): Boolean = card.batchId == VOLT_INU_WALLET_BATCH_ID
override fun isVividWallet(): Boolean = card.batchId == VIVID_LEMON_WALLET_BATCH_ID ||
card.batchId == VIVID_AQUA_WALLET_BATCH_ID ||
card.batchId == VIVID_GRAPEFRUIT_WALLET_BATCH_ID
override fun isPastelWallet(): Boolean = card.batchId == PASTEL_PEACH_WALLET_BATCH_ID ||
card.batchId == PASTEL_GRASS_WALLET_BATCH_ID ||
card.batchId == PASTEL_AIR_WALLET_BATCH_ID
override fun isWhiteWallet(): Boolean {
return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable
}
@ -183,34 +133,5 @@ internal class TangemCardTypesResolver(
private companion object {
const val DEV_KIT_CARD_BATCH_ID = "CB83"
const val TRON_WALLET_BATCH_ID = "AF07"
const val KASPA_WALLET_BATCH_ID = "AF08"
const val KASPA2_WALLET_BATCH_ID = "AF25"
const val KASPA_RESELLER_WALLET_BATCH_ID = "AF31"
const val BAD_WALLET_BATCH_ID = "AF09"
const val JR_WALLET_BATCH_ID = "AF14"
const val GRIM_WALLET_BATCH_ID = "AF13"
const val SATOSHI_WALLET_BATCH_ID = "AF19"
const val WHITE_WALLET2_BATCH_ID = "AF15"
const val TRILLIANT_WALLET_BATCH_ID = "AF16"
const val AVRORA_WALLET_BATCH_ID = "AF18"
const val BITCOIN_PIZZA_DAY_WALLET_BATCH_ID = "AF33"
const val VECHAIN_WALLET_BATCH_ID = "AF29"
const val NEW_WORLD_ELITE_WALLET_BATCH_ID = "AF26"
const val RED_PANDA_WALLET_BATCH_ID = "AF34"
const val CRYPTO_SETH_WALLET_BATCH_ID = "AF32"
const val KISHU_INU_WALLET_BATCH_ID = "AF52"
const val BABY_DOGE_WALLET_BATCH_ID = "AF51"
const val COQ_WALLET_BATCH_ID = "AF28"
const val COIN_METRICA_WALLET_BATCH_ID = "AF27"
const val VOLT_INU_WALLET_BATCH_ID = "AF35"
// VIVID WALLETS
const val VIVID_LEMON_WALLET_BATCH_ID = "AF40"
const val VIVID_AQUA_WALLET_BATCH_ID = "AF41"
const val VIVID_GRAPEFRUIT_WALLET_BATCH_ID = "AF42"
// PASTEL WALLETS
const val PASTEL_PEACH_WALLET_BATCH_ID = "AF43"
const val PASTEL_AIR_WALLET_BATCH_ID = "AF44"
const val PASTEL_GRASS_WALLET_BATCH_ID = "AF45"
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.configs.Wallet2CardConfig
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
@ -32,8 +33,6 @@ val UserWallet.cardTypesResolver: CardTypesResolver
get() = scanResponse.cardTypesResolver
fun ScanResponse.twinsIsTwinned(): Boolean = card.isTangemTwins && walletData != null && secondTwinPublicKey != null
fun ScanResponse.supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed
fun ScanResponse.supportsBackup(): Boolean = card.settings.isBackupAllowed
fun ScanResponse.hasDerivation(blockchain: Blockchain, rawDerivationPath: String): Boolean {
return hasDerivation(blockchain, DerivationPath(rawDerivationPath))
@ -66,4 +65,37 @@ private fun ScanResponse.hasDerivation(curve: EllipticCurve, derivationPath: Der
val extendedPublicKeysMap = derivedKeys[foundWallet.publicKey.toMapKey()] ?: return false
val extendedPublicKey = extendedPublicKeysMap[derivationPath]
return extendedPublicKey != null
}
/**
* Get total cards count in wallets set for this [ScanResponse] card
*
* @return null if wallet is not multi-currency or total cards count
*/
fun ScanResponse.getCardsCount(): Int? {
if (!cardTypesResolver.isMultiwalletAllowed()) return null
return when (val status = card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount + 1
is CardDTO.BackupStatus.NoBackup,
is CardDTO.BackupStatus.CardLinked,
null, // Multi-currency wallet without backup function. Example, 4.12
-> 1
}
}
/**
* Get backup cards count for this [ScanResponse] card
*
* @return null if wallet is not multi-currency or total cards count
*/
fun ScanResponse.getBackupCardsCount(): Int? {
return if (cardTypesResolver.isMultiwalletAllowed()) {
when (val status = card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount
else -> 0
}
} else {
null
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.domain.common.util
import com.tangem.domain.wallets.models.UserWallet
/**
* Get total cards count in wallets set for a card that was saved in [UserWallet]
*
* @return null if wallet is not multi-currency or total cards count
*/
fun UserWallet.getCardsCount(): Int? = scanResponse.getCardsCount()
/**
* Get backup cards count for a card that was saved in [UserWallet]
*
* @return null if wallet is not multi-currency or total cards count
*/
fun UserWallet.getBackupCardsCount(): Int? = scanResponse.getBackupCardsCount()

View file

@ -376,15 +376,12 @@ class DefaultWalletManagersFacade(
return walletManagersStore.getAllSync(userWalletId)
}
@Deprecated(
"Use NetworkAddress from CryptoCurrencyStatus",
ReplaceWith("cryptoCurrencyStatus.value.networkAddress"),
)
override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List<Address> {
return getAddresses(userWalletId, network).sortedBy { it.type }
override suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String? {
return getAddresses(userWalletId, network)
.firstOrNull { it.type == AddressType.Default }
?.value
}
@Deprecated("Use NetworkAddress from CryptoCurrencyStatus")
override suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set<Address> {
val manager = getOrCreateWalletManager(
userWalletId = userWalletId,

View file

@ -117,20 +117,18 @@ interface WalletManagersFacade {
suspend fun getStoredWalletManagers(userWalletId: UserWalletId): List<WalletManager>
/**
* Returns ordered list of addresses for selected wallet for given currency
* Returns default network address for selected wallet in given network
*
* @param userWalletId selected wallet id
* @param network network of currency
*/
@Deprecated("Use NetworkAddress from CryptoCurrencyStatus")
suspend fun getAddress(userWalletId: UserWalletId, network: Network): List<Address>
suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String?
/** Returns list of all addresses for all currencies in selected wallet
*
* @param userWalletId selected wallet id
* @param network required to create wallet manager
*/
@Deprecated("Use NetworkAddress from CryptoCurrencyStatus")
suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set<Address>
/**

View file

@ -11,12 +11,15 @@ android {
dependencies {
/* Domain */
api(projects.domain.appCurrency.models)
api(projects.domain.core)
api(projects.core.pagination)
api(projects.domain.markets.models)
implementation(deps.kotlin.serialization)
implementation(projects.domain.tokens.models)
implementation(projects.domain.tokens)
/* Utils */
implementation(deps.kotlin.serialization)
implementation(projects.core.utils)
}

View file

@ -1,35 +1,32 @@
package com.tangem.features.markets.details.api
package com.tangem.domain.markets
import com.tangem.domain.core.serialization.SerializedBigDecimal
import com.tangem.domain.markets.TokenMarket
import kotlinx.serialization.Serializable
@Serializable
data class TokenMarketSerializable(
data class TokenMarketParams(
val id: String,
val name: String,
val symbol: String,
val marketCap: SerializedBigDecimal?,
val tokenQuotes: Quotes,
val imageUrl: String,
val imageUrl: String?,
) {
@Serializable
data class Quotes(
val currentPrice: SerializedBigDecimal,
val h24Percent: SerializedBigDecimal,
val weekPercent: SerializedBigDecimal,
val monthPercent: SerializedBigDecimal,
val weekPercent: SerializedBigDecimal?,
val monthPercent: SerializedBigDecimal?,
)
}
fun TokenMarket.toSerializable(): TokenMarketSerializable {
return TokenMarketSerializable(
fun TokenMarket.toSerializableParam(): TokenMarketParams {
return TokenMarketParams(
id = id,
name = name,
symbol = symbol,
marketCap = marketCap,
tokenQuotes = TokenMarketSerializable.Quotes(
tokenQuotes = TokenMarketParams.Quotes(
currentPrice = tokenQuotesShort.currentPrice,
h24Percent = tokenQuotesShort.h24ChangePercent,
weekPercent = tokenQuotesShort.weekChangePercent,

View file

@ -0,0 +1,18 @@
package com.tangem.domain.markets
import arrow.core.Either
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.repositories.MarketsTokenRepository
class GetTokenFullQuotesUseCase(
private val marketsTokenRepository: MarketsTokenRepository,
) {
suspend operator fun invoke(appCurrency: AppCurrency, tokenId: String): Either<Unit, TokenQuotes> {
return Either.catch {
marketsTokenRepository.getTokenQuotes(
fiatCurrencyCode = appCurrency.code,
tokenId = tokenId,
)
}.mapLeft {}
}
}

View file

@ -12,13 +12,22 @@ class GetTokenPriceChartUseCase(
appCurrency: AppCurrency,
interval: PriceChangeInterval,
tokenId: String,
preview: Boolean,
): Either<Unit, TokenChart> {
return Either.catch {
marketsTokenRepository.getChart(
fiatCurrencyCode = appCurrency.code,
interval = interval,
tokenId = tokenId,
)
if (preview) {
marketsTokenRepository.getChartPreview(
fiatCurrencyCode = appCurrency.code,
interval = interval,
tokenId = tokenId,
)
} else {
marketsTokenRepository.getChart(
fiatCurrencyCode = appCurrency.code,
interval = interval,
tokenId = tokenId,
)
}
}.mapLeft {}
}
}

View file

@ -1,19 +1,26 @@
package com.tangem.domain.markets
import arrow.core.Either
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.repository.QuotesRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
@Suppress("UnusedPrivateMember")
class GetTokenQuotesUseCase(
private val marketsTokenRepository: MarketsTokenRepository,
private val quotesRepository: QuotesRepository,
) {
suspend operator fun invoke(appCurrency: AppCurrency, tokenId: String): Either<Unit, TokenQuotes> {
return Either.catch {
marketsTokenRepository.getTokenQuotes(
fiatCurrencyCode = appCurrency.code,
tokenId = tokenId,
)
}.mapLeft {}
operator fun invoke(tokenId: String, interval: PriceChangeInterval): Flow<Either<Unit, Quote>> {
return flowOf(
Either.catch {
Quote(
rawCurrencyId = "USD",
fiatRate = 100.toBigDecimal(), // mock
priceChange = 10.toBigDecimal(), // mock
)
}.mapLeft {},
)
// TODO implement quotes fetching from repository [REDACTED_TASK_KEY]
// quotesRepository.getQuotesUpdates()
}
}

View file

@ -12,6 +12,8 @@ interface MarketsTokenRepository {
suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart
suspend fun getChartPreview(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart
suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String, languageCode: String): TokenMarketInfo
suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes

View file

@ -1,9 +1,10 @@
package com.tangem.domain.staking.model
import com.tangem.domain.staking.model.stakekit.Yield
import java.math.BigDecimal
data class StakingEntryInfo(
val interestRate: BigDecimal,
val periodInDays: Int,
val apr: BigDecimal,
val rewardSchedule: Yield.Metadata.RewardSchedule,
val tokenSymbol: String,
)

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