Updated on 2026-08-14
This commit is contained in:
commit
43c376489a
316 changed files with 7586 additions and 3131 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)?
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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(
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
14
core/ui/src/main/res/drawable/ic_tether_24.xml
Normal file
14
core/ui/src/main/res/drawable/ic_tether_24.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class SaveBlockchainErrorUseCase(
|
|||
private val feedbackRepository: FeedbackRepository,
|
||||
) {
|
||||
|
||||
fun invoke(error: BlockchainErrorInfo) {
|
||||
operator fun invoke(error: BlockchainErrorInfo) {
|
||||
feedbackRepository.saveBlockchainErrorInfo(error = error)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue