Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-27 09:52:31 +03:00
commit 67be280129
26 changed files with 227 additions and 153 deletions

View file

@ -55,7 +55,7 @@
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/SplashTheme"
android:windowSoftInputMode="adjustNothing">
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

@ -4,8 +4,10 @@ import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
@ -215,6 +217,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
super.onCreate(savedInstanceState)
// We have to allow adjust_resize (can only be specified in the manifest) for Android <=10,
// in order for imePadding to work correctly
// for Android 11+ we set SOFT_INPUT_ADJUST_NOTHING to prevent resizing the layout
// so that we don't have any distortions in the layout when displaying the keyboard
// https://issuetracker.google.com/issues/266331465
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
}
splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown }
installActivityDependencies()

View file

@ -113,12 +113,14 @@ internal object TokensDomainModule {
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
): GetAllWalletsCryptoCurrencyStatusesUseCase {
return GetAllWalletsCryptoCurrencyStatusesUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
dispatchers = dispatchers,
)
}

View file

@ -59,13 +59,13 @@
<item name="android:minHeight">48dp</item>
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>
<item name="cornerRadius">14dp</item>
<item name="cornerRadius">16dp</item>
<item name="android:textAllCaps">false</item>
<item name="android:textSize">14sp</item>
<item name="android:textSize">16sp</item>
<item name="android:fontFamily">@font/roboto_medium</item>
<item name="android:letterSpacing">0.01</item>
<item name="android:letterSpacing">0.05</item>
<item name="fontWeight">500</item>
<item name="lineHeight">16sp</item>
<item name="lineHeight">24sp</item>
<item name="iconPadding">4dp</item>
</style>

View file

@ -808,6 +808,7 @@
<string name="transaction_history_operation">Operation</string>
<string name="transaction_history_transaction_from_address">von: %s</string>
<string name="transaction_history_transaction_to_address">zu: %s</string>
<string name="transaction_history_transaction_validator">Validierer: %s</string>
<string name="try_to_load_data_again_button_title">Versuche es erneut</string>
<string name="twin_error_same_card">Du hast dieselbe Karte gescannt. Um ein Zwillings-Wallet zu erstellen, musst du die Karte mit der Nummer %d scannen.</string>
<string name="twin_error_wrong_twin">Du hast die falsche Doppelkarte gescannt. Bitte versuche eine andere Karte</string>

View file

@ -793,7 +793,7 @@
<string name="token_details_staking_block_subtitle">El staking le permite ganar %1$s y obtener recompensas cada %2$s días</string>
<string name="token_details_staking_block_title">Gane hasta %s recompensa del staking por año</string>
<string name="token_details_token_type_subtitle">Token de %1$s en la red %%image%% %2$s</string>
<string name="token_details_token_type_subtitle_no_standard" formatted="false">Token en la %image% red %1$s</string>
<string name="token_details_token_type_subtitle_no_standard">Token en la %%image%% red %1$s</string>
<string name="token_details_unable_hide_alert_message">El token %1$s (%2$s) es la moneda principal en la red %3$s y no se puede ocultar mientras tengas otros tokens de esta red en la lista</string>
<string name="token_details_unable_hide_alert_title">No se puede ocultar %s</string>
<string name="token_swap_changelly_promotion_message">Cambie este token por otro por una tarifa de servicio de %1$s del %2$s al %3$s de febrero.</string>

View file

@ -18,6 +18,7 @@ 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.common.extensions.canHandleBlockchain
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.supportedBlockchains
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
@ -208,6 +209,8 @@ internal class DefaultCustomTokensRepository(
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = scanResponse.derivationStyleProvider,
)?.copy(
canHandleTokens = scanResponse.card.canHandleToken(blockchain, scanResponse.cardTypesResolver),
)
} else {
null

View file

@ -50,9 +50,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -292,14 +292,18 @@ internal class DefaultStakingRepository(
) = withContext(dispatchers.io) {
if (!stakingFeatureToggle.isStakingEnabled) 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 integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)]
val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network)
if (integrationId == null || address.isNullOrBlank()) {
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
error("IntegrationId or address is null")
}
val requestBody = getBalanceRequestData(address, integrationId)
val result = stakeKitApi.getSingleYieldBalance(
integrationId = requestBody.integrationId,
@ -307,10 +311,10 @@ internal class DefaultStakingRepository(
).getOrThrow()
stakingBalanceStore.store(
userWalletId,
requestBody.integrationId,
address,
YieldBalanceWrapperDTO(
userWalletId = userWalletId,
integrationId = requestBody.integrationId,
address = address,
item = YieldBalanceWrapperDTO(
balances = result,
integrationId = requestBody.integrationId,
addresses = requestBody.addresses,
@ -396,7 +400,10 @@ internal class DefaultStakingRepository(
addresses.map { address -> address to integrationId }
}
.map { getBalanceRequestData(it.first.value, it.second) }
.ifEmpty { return@invokeOnExpire }
.ifEmpty {
cacheRegistry.invalidate(getYieldBalancesKey(userWalletId))
error("No addresses found")
}
val result = stakeKitApi.getMultipleYieldBalances(availableCurrencies).getOrThrow()
stakingBalanceStore.store(userWalletId, result)

View file

@ -19,25 +19,23 @@ enum class StakingActionType {
UNKNOWN,
;
companion object {
val StakingActionType.asAnalyticName
get() = when (this) {
STAKE -> "Stake"
UNSTAKE -> "Unstake"
CLAIM_REWARDS -> "Claim Rewards"
RESTAKE_REWARDS -> "Restake Rewards"
WITHDRAW -> "Withdraw"
RESTAKE -> "Restake"
CLAIM_UNSTAKED -> "Claim Unstaked"
UNLOCK_LOCKED -> "Unlock Locked"
STAKE_LOCKED -> "Stake Locked"
VOTE -> "Vote"
REVOKE -> "Revoke"
VOTE_LOCKED -> "Vote Locked"
REVOTE -> "Revote"
REBOND -> "Rebond"
MIGRATE -> "Migrate"
UNKNOWN -> "Unknown"
}
}
val asAnalyticName
get() = when (this) {
STAKE -> "Stake"
UNSTAKE -> "Unstake"
CLAIM_REWARDS -> "Claim Rewards"
RESTAKE_REWARDS -> "Restake Rewards"
WITHDRAW -> "Withdraw"
RESTAKE -> "Restake"
CLAIM_UNSTAKED -> "Claim Unstaked"
UNLOCK_LOCKED -> "Unlock Locked"
STAKE_LOCKED -> "Stake Locked"
VOTE -> "Vote"
REVOKE -> "Revoke"
VOTE_LOCKED -> "Vote Locked"
REVOTE -> "Revote"
REBOND -> "Rebond"
MIGRATE -> "Migrate"
UNKNOWN -> "Unknown"
}
}

View file

@ -11,6 +11,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
@ -21,6 +22,7 @@ import kotlinx.coroutines.flow.*
* @property quotesRepository quotes repository
* @property networksRepository networks repository
* @property stakingRepository staking repository
* @property dispatchers dispatchers
*
[REDACTED_AUTHOR]
*/
@ -29,6 +31,7 @@ class GetAllWalletsCryptoCurrencyStatusesUseCase(
private val quotesRepository: QuotesRepository,
private val networksRepository: NetworksRepository,
private val stakingRepository: StakingRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
/**
@ -62,5 +65,6 @@ class GetAllWalletsCryptoCurrencyStatusesUseCase(
combine(walletStatusFlows) { it.toMap() }
}
.flowOn(dispatchers.io)
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.features.managetokens.component
enum class ManageTokensSource {
STORIES,
ONBOARDING,
SETTINGS,
enum class ManageTokensSource(val analyticsName: String) {
STORIES(analyticsName = "Stories"),
ONBOARDING(analyticsName = "Onboarding"),
SETTINGS(analyticsName = "Settings"),
}

View file

@ -15,7 +15,7 @@ internal sealed class CustomTokenAnalyticsEvent(
class ScreenOpened(source: ManageTokensSource) : CustomTokenAnalyticsEvent(
event = "Custom Token Screen Opened",
params = mapOf(AnalyticsParam.Key.SOURCE to source.name),
params = mapOf(AnalyticsParam.Key.SOURCE to source.analyticsName),
)
class CustomTokenWasAdded(
@ -27,7 +27,7 @@ internal sealed class CustomTokenAnalyticsEvent(
params = mapOf(
AnalyticsParam.Key.TOKEN_PARAM to currencySymbol,
AnalyticsParam.Key.DERIVATION to derivationPath,
AnalyticsParam.Key.SOURCE to source.name,
AnalyticsParam.Key.SOURCE to source.analyticsName,
),
)
@ -35,7 +35,7 @@ internal sealed class CustomTokenAnalyticsEvent(
event = "Custom Token Network Selected",
params = mapOf(
AnalyticsParam.Key.BLOCKCHAIN to networkName,
AnalyticsParam.Key.SOURCE to source.name,
AnalyticsParam.Key.SOURCE to source.analyticsName,
),
)
@ -43,7 +43,7 @@ internal sealed class CustomTokenAnalyticsEvent(
event = "Custom Token Derivation Selected",
params = mapOf(
AnalyticsParam.Key.DERIVATION to derivationName,
AnalyticsParam.Key.SOURCE to source.name,
AnalyticsParam.Key.SOURCE to source.analyticsName,
),
)
@ -51,27 +51,27 @@ internal sealed class CustomTokenAnalyticsEvent(
event = "Custom Token Address",
params = mapOf(
AnalyticsParam.Key.VALIDATION to AnalyticsParam.Validation.from(isValid),
AnalyticsParam.Key.SOURCE to source.name,
AnalyticsParam.Key.SOURCE to source.analyticsName,
),
)
class Name(source: ManageTokensSource) : CustomTokenAnalyticsEvent(
event = "Custom Token Name",
params = mapOf(AnalyticsParam.Key.SOURCE to source.name),
params = mapOf(AnalyticsParam.Key.SOURCE to source.analyticsName),
)
class Symbol(source: ManageTokensSource) : CustomTokenAnalyticsEvent(
event = "Custom Token Symbol",
params = mapOf(AnalyticsParam.Key.SOURCE to source.name),
params = mapOf(AnalyticsParam.Key.SOURCE to source.analyticsName),
)
class Decimals(source: ManageTokensSource) : CustomTokenAnalyticsEvent(
event = "Custom Token Decimals",
params = mapOf(AnalyticsParam.Key.SOURCE to source.name),
params = mapOf(AnalyticsParam.Key.SOURCE to source.analyticsName),
)
class ButtonCustomToken(source: ManageTokensSource) : CustomTokenAnalyticsEvent(
event = "Button - Custom Token",
params = mapOf(AnalyticsParam.Key.SOURCE to source.name),
params = mapOf(AnalyticsParam.Key.SOURCE to source.analyticsName),
)
}

View file

@ -8,21 +8,21 @@ internal sealed class ManageTokensAnalyticEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(
category = "ManageTokens",
category = "Manage Tokens",
event = event,
params = params,
) {
class ScreenOpened(source: ManageTokensSource) : ManageTokensAnalyticEvent(
event = "Manage Tokens Screen Opened",
params = mapOf(AnalyticsParam.Key.SOURCE to source.name),
params = mapOf(AnalyticsParam.Key.SOURCE to source.analyticsName),
)
class TokensIsNotFound(query: String, source: ManageTokensSource) : ManageTokensAnalyticEvent(
event = "Token Is Not Found",
params = mapOf(
AnalyticsParam.Key.INPUT to query,
AnalyticsParam.Key.SOURCE to source.name,
AnalyticsParam.Key.SOURCE to source.analyticsName,
),
)
@ -35,7 +35,7 @@ internal sealed class ManageTokensAnalyticEvent(
params = mapOf(
AnalyticsParam.Key.TOKEN_PARAM to tokenSymbol,
AnalyticsParam.Key.STATE to AnalyticsParam.OnOffState.from(isSelected),
AnalyticsParam.Key.SOURCE to source.name,
AnalyticsParam.Key.SOURCE to source.analyticsName,
),
)
@ -46,7 +46,7 @@ internal sealed class ManageTokensAnalyticEvent(
event = "Token Added",
params = mapOf(
AnalyticsParam.Key.COUNT to tokensCount.toString(),
AnalyticsParam.Key.SOURCE to source.name,
AnalyticsParam.Key.SOURCE to source.analyticsName,
),
)

View file

@ -22,6 +22,7 @@ import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
import com.tangem.features.managetokens.ui.AddCustomTokenBottomSheet
import com.tangem.features.managetokens.utils.ui.toContentModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -34,15 +35,16 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
) : AddCustomTokenComponent, AppComponentContext by context {
private val initialConfiguration = AddCustomTokenConfig(
userWalletId = params.userWalletId,
step = AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR,
)
private val navigation = StackNavigation<AddCustomTokenConfig>()
private val contentStack = childStack(
key = "add_custom_token_content_stack",
source = navigation,
initialConfiguration = AddCustomTokenConfig(
userWalletId = params.userWalletId,
step = AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR,
popBack = ::dismiss,
),
initialConfiguration = initialConfiguration,
handleBackButton = true,
serializer = AddCustomTokenConfig.serializer(),
childFactory = ::contentChild,
@ -62,14 +64,14 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
TangemBottomSheetConfig(
isShow = true,
onDismissRequest = ::dismiss,
content = contentStack.active.configuration,
content = initialConfiguration.step.toContentModel(::popBack),
)
}
val childStack by contentStack.subscribeAsState()
AddCustomTokenBottomSheet(
config = config.copy(
content = childStack.active.configuration,
content = childStack.active.configuration.step.toContentModel(::popBack),
),
content = { modifier ->
Children(
@ -82,6 +84,17 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
)
}
private fun popBack() {
when (contentStack.value.active.configuration.step) {
AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR,
AddCustomTokenConfig.Step.FORM,
-> dismiss()
AddCustomTokenConfig.Step.NETWORK_SELECTOR,
AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR,
-> navigation.pop()
}
}
private fun contentChild(
config: AddCustomTokenConfig,
componentContext: ComponentContext,
@ -164,7 +177,6 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
val config = currentConfig.copy(
step = AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR,
formValues = formValues,
popBack = navigation::pop,
)
navigation.push(config)
}
@ -175,7 +187,6 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
val config = currentConfig.copy(
step = AddCustomTokenConfig.Step.NETWORK_SELECTOR,
formValues = formValues,
popBack = navigation::pop,
)
navigation.push(config)
}
@ -187,7 +198,6 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
step = AddCustomTokenConfig.Step.FORM,
selectedNetwork = network ?: currentConfig.selectedNetwork,
selectedDerivationPath = derivationPath ?: currentConfig.selectedDerivationPath,
popBack = ::dismiss,
)
navigation.replaceAll(config)
}

View file

@ -9,17 +9,17 @@ import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig
import com.tangem.features.managetokens.ui.AddCustomTokenBottomSheet
import com.tangem.features.managetokens.utils.ui.toContentModel
import kotlinx.coroutines.flow.MutableStateFlow
internal class PreviewAddCustomTokenComponent(
initialState: AddCustomTokenConfig = AddCustomTokenConfig(
userWalletId = UserWalletId(stringValue = "321"),
step = AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR,
popBack = {},
),
) : AddCustomTokenComponent {
private val previewState: MutableStateFlow<AddCustomTokenConfig> = MutableStateFlow(initialState)
private val previewConfig: MutableStateFlow<AddCustomTokenConfig> = MutableStateFlow(initialState)
override fun dismiss() {
/* no-op */
@ -27,21 +27,21 @@ internal class PreviewAddCustomTokenComponent(
@Composable
override fun BottomSheet() {
val state by previewState.collectAsStateWithLifecycle()
val config = TangemBottomSheetConfig(
val config by previewConfig.collectAsStateWithLifecycle()
val bottomSheetConfig = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = ::dismiss,
content = state,
content = config.step.toContentModel(::dismiss),
)
AddCustomTokenBottomSheet(
config = config,
config = bottomSheetConfig,
content = { modifier ->
when (state.step) {
when (config.step) {
AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR -> {
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.NetworkSelector(
userWalletId = state.userWalletId,
userWalletId = config.userWalletId,
selectedNetwork = null,
onNetworkSelected = {},
),
@ -50,8 +50,8 @@ internal class PreviewAddCustomTokenComponent(
AddCustomTokenConfig.Step.NETWORK_SELECTOR -> {
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.NetworkSelector(
userWalletId = state.userWalletId,
selectedNetwork = state.selectedNetwork,
userWalletId = config.userWalletId,
selectedNetwork = config.selectedNetwork,
onNetworkSelected = {},
),
).Content(modifier)
@ -59,9 +59,9 @@ internal class PreviewAddCustomTokenComponent(
AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR -> {
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.DerivationPathSelector(
userWalletId = state.userWalletId,
selectedNetwork = state.selectedNetwork!!,
selectedDerivationPath = state.selectedDerivationPath!!,
userWalletId = config.userWalletId,
selectedNetwork = config.selectedNetwork!!,
selectedDerivationPath = config.selectedDerivationPath!!,
onDerivationPathSelected = {},
),
).Content(modifier)

View file

@ -1,6 +1,5 @@
package com.tangem.features.managetokens.entity.customtoken
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable
@ -8,12 +7,11 @@ import kotlinx.serialization.Serializable
@Serializable
internal data class AddCustomTokenConfig(
val step: Step,
val popBack: () -> Unit,
val userWalletId: UserWalletId,
val selectedNetwork: SelectedNetwork? = null,
val selectedDerivationPath: SelectedDerivationPath? = null,
val formValues: CustomTokenFormValues = CustomTokenFormValues(),
) : TangemBottomSheetConfigContent {
) {
enum class Step {
INITIAL_NETWORK_SELECTOR,

View file

@ -0,0 +1,10 @@
package com.tangem.features.managetokens.entity.customtoken
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
internal data class AddCustomTokenUM(
val popBack: () -> Unit,
val title: TextReference,
val showBackButton: Boolean,
) : TangemBottomSheetConfigContent

View file

@ -1,5 +1,6 @@
package com.tangem.features.managetokens.model
import androidx.annotation.StringRes
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
@ -105,7 +106,7 @@ internal class ManageTokensModel @Inject constructor(
onBackButtonClick = router::pop,
),
search = SearchBarUM(
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
placeholderText = resourceReference(R.string.common_search),
query = "",
onQueryChange = ::searchCurrencies,
isActive = false,
@ -130,7 +131,7 @@ internal class ManageTokensModel @Inject constructor(
),
),
search = SearchBarUM(
placeholderText = resourceReference(R.string.manage_tokens_search_placeholder),
placeholderText = resourceReference(R.string.common_search),
query = "",
onQueryChange = ::searchCurrencies,
isActive = false,
@ -321,8 +322,15 @@ internal class ManageTokensModel @Inject constructor(
private fun toggleSearchBar(isActive: Boolean) {
state.update { state ->
@StringRes val placeholderTextRes = if (isActive) {
R.string.manage_tokens_search_placeholder
} else {
R.string.common_search
}
state.copySealed(
search = state.search.copy(
placeholderText = resourceReference(placeholderTextRes),
isActive = isActive,
),
)

View file

@ -14,8 +14,6 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.tokens.model.Network
@ -23,13 +21,13 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.preview.PreviewAddCustomTokenComponent
import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig
import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenUM
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
import com.tangem.features.managetokens.impl.R
@Composable
internal fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig, content: @Composable (Modifier) -> Unit) {
TangemBottomSheet<AddCustomTokenConfig>(
TangemBottomSheet<AddCustomTokenUM>(
config = config,
addBottomInsets = false,
title = { model ->
@ -47,35 +45,14 @@ internal fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig, content:
}
@Composable
private fun Title(model: AddCustomTokenConfig, modifier: Modifier = Modifier) {
when (model.step) {
AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR,
AddCustomTokenConfig.Step.FORM,
-> {
TangemBottomSheetTitle(
modifier = modifier,
title = resourceReference(R.string.add_custom_token_title),
)
}
AddCustomTokenConfig.Step.NETWORK_SELECTOR -> {
TangemTopAppBar(
modifier = modifier,
title = resourceReference(R.string.custom_token_network_selector_title),
titleAlignment = Alignment.CenterHorizontally,
startButton = TopAppBarButtonUM.Back(model.popBack),
height = TangemTopAppBarHeight.BOTTOM_SHEET,
)
}
AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR -> {
TangemTopAppBar(
modifier = modifier,
title = resourceReference(R.string.custom_token_derivation_path),
titleAlignment = Alignment.CenterHorizontally,
startButton = TopAppBarButtonUM.Back(model.popBack),
height = TangemTopAppBarHeight.BOTTOM_SHEET,
)
}
}
private fun Title(model: AddCustomTokenUM, modifier: Modifier = Modifier) {
TangemTopAppBar(
modifier = modifier,
title = model.title,
titleAlignment = Alignment.CenterHorizontally,
startButton = TopAppBarButtonUM.Back(model.popBack).takeIf { model.showBackButton },
height = TangemTopAppBarHeight.BOTTOM_SHEET,
)
}
// region Preview
@ -98,7 +75,6 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<
initialState = AddCustomTokenConfig(
userWalletId = UserWalletId(stringValue = "321"),
step = AddCustomTokenConfig.Step.FORM,
popBack = {},
selectedNetwork = SelectedNetwork(
id = Network.ID(value = "1"),
name = "Ethereum",
@ -111,7 +87,6 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<
initialState = AddCustomTokenConfig(
userWalletId = UserWalletId(stringValue = "321"),
step = AddCustomTokenConfig.Step.NETWORK_SELECTOR,
popBack = {},
selectedNetwork = SelectedNetwork(
id = Network.ID(value = "0"),
name = "Ethereum",
@ -124,7 +99,6 @@ private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider<
initialState = AddCustomTokenConfig(
userWalletId = UserWalletId(stringValue = "321"),
step = AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR,
popBack = {},
selectedDerivationPath = SelectedDerivationPath(
id = Network.ID(value = "0"),
value = Network.DerivationPath.None,

View file

@ -0,0 +1,26 @@
package com.tangem.features.managetokens.utils.ui
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig
import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenUM
import com.tangem.features.managetokens.impl.R
internal fun AddCustomTokenConfig.Step.toContentModel(popBack: () -> Unit): AddCustomTokenUM = when (this) {
AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR,
AddCustomTokenConfig.Step.FORM,
-> AddCustomTokenUM(
popBack = popBack,
title = resourceReference(R.string.add_custom_token_title),
showBackButton = false,
)
AddCustomTokenConfig.Step.NETWORK_SELECTOR -> AddCustomTokenUM(
popBack = popBack,
title = resourceReference(R.string.custom_token_network_selector_title),
showBackButton = true,
)
AddCustomTokenConfig.Step.DERIVATION_PATH_SELECTOR -> AddCustomTokenUM(
popBack = popBack,
title = resourceReference(R.string.custom_token_derivation_path),
showBackButton = true,
)
}

View file

@ -7,15 +7,14 @@ import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetContentUM
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
import com.tangem.features.markets.tokenlist.impl.ui.state.*
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
@Stable
internal class MarketsListUMStateManager(
@ -193,7 +192,7 @@ internal class MarketsListUMStateManager(
private fun state(): MarketsListUM = MarketsListUM(
list = ListUM.Loading,
searchBar = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
placeholderText = resourceReference(R.string.markets_search_header_title),
query = "",
onQueryChange = { searchQuery = it },
isActive = false,

View file

@ -40,11 +40,11 @@ import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet
import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByBottomSheetContentUM
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
import com.tangem.features.markets.tokenlist.impl.ui.preview.MarketChartListItemPreviewDataProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -310,7 +310,7 @@ private fun Preview() {
onItemClick = {},
),
searchBar = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
placeholderText = resourceReference(R.string.markets_search_header_title),
query = "",
onQueryChange = {},
isActive = false,

View file

@ -3,7 +3,6 @@ package com.tangem.features.staking.impl.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.staking.model.stakekit.action.StakingActionType.Companion.asAnalyticName
internal sealed class StakingAnalyticsEvents(
event: String,
@ -101,6 +100,17 @@ internal sealed class StakingAnalyticsEvents(
),
)
data class ValidatorChosen(
val token: String,
val validator: String,
) : StakingAnalyticsEvents(
event = "Validator Chosen",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
"Validator" to validator,
),
)
data class ButtonValidator(
val source: StakeScreenSource,
val token: String,
@ -122,11 +132,11 @@ internal sealed class StakingAnalyticsEvents(
)
data class ButtonAction(
val action: String,
val action: StakingActionType,
val token: String,
val validator: String,
) : StakingAnalyticsEvents(
event = "Button - $action",
event = "Button - ${action.asAnalyticName}",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to token,
"Validator" to validator,

View file

@ -82,14 +82,6 @@ internal class StakingAnalyticSender(
val validatorState = confirmationState?.validatorState as? ValidatorState.Content
val validatorName = validatorState?.chosenValidator?.name ?: return
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonAction(
action = getStakingActionType(value).name,
token = value.cryptoCurrencyName,
validator = validatorName,
),
)
analyticsEventHandler.send(
Basic.TransactionSent(
sentFrom = AnalyticsParam.TxSentFrom.Staking(
@ -109,6 +101,20 @@ internal class StakingAnalyticSender(
)
}
fun sendTransactionStakingClickedAnalytics(value: StakingUiState) {
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
val validatorState = confirmationState?.validatorState as? ValidatorState.Content
val validatorName = validatorState?.chosenValidator?.name ?: return
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonAction(
action = getStakingActionType(value),
token = value.cryptoCurrencySymbol,
validator = validatorName,
),
)
}
private fun getStakingActionType(value: StakingUiState): StakingActionType {
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data

View file

@ -262,7 +262,7 @@ internal class StakingViewModel @Inject constructor(
onStakingFeeError = { error ->
analyticsEventHandler.send(
StakingAnalyticsEvents.StakingError(
value.cryptoCurrencyName,
value.cryptoCurrencySymbol,
error.javaClass.simpleName,
),
)
@ -270,7 +270,7 @@ internal class StakingViewModel @Inject constructor(
updateNotifications(GetFeeError.UnknownError)
},
onFeeError = { error ->
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencyName))
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol))
stateController.update(AddStakingErrorTransformer())
updateNotifications(error)
},
@ -291,6 +291,7 @@ internal class StakingViewModel @Inject constructor(
private fun handleOnNextConfirmationClick() {
if (isAssentState()) {
viewModelScope.launch {
stakingAnalyticSender.sendTransactionStakingClickedAnalytics(value)
stateController.update(SetConfirmationStateInProgressTransformer())
transactionSender.constructAndSendTransactions(
onConstructSuccess = { constructedTransactions ->
@ -300,7 +301,7 @@ internal class StakingViewModel @Inject constructor(
Timber.e(error.toString())
analyticsEventHandler.send(
StakingAnalyticsEvents.StakingError(
token = value.cryptoCurrencyName,
token = value.cryptoCurrencySymbol,
errorType = error.javaClass.simpleName,
),
)
@ -314,7 +315,7 @@ internal class StakingViewModel @Inject constructor(
},
onSendError = { error ->
Timber.e(error.toString())
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencyName))
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol))
stakingEventFactory.createSendTransactionErrorAlert(error)
stateController.update(SetConfirmationStateResetAssentTransformer)
},
@ -353,7 +354,7 @@ internal class StakingViewModel @Inject constructor(
}
override fun onInitialInfoBannerClick() {
analyticsEventHandler.send(StakingAnalyticsEvents.WhatIsStaking(cryptoCurrencyStatus.currency.symbol))
analyticsEventHandler.send(StakingAnalyticsEvents.WhatIsStaking(value.cryptoCurrencySymbol))
innerRouter.openUrl(WHAT_IS_STAKING_ARTICLE_URL)
}
@ -374,13 +375,13 @@ internal class StakingViewModel @Inject constructor(
}
override fun onMaxValueClick() {
analyticsEventHandler.send(StakingAnalyticsEvents.ButtonMax(cryptoCurrencyStatus.currency.symbol))
analyticsEventHandler.send(StakingAnalyticsEvents.ButtonMax(value.cryptoCurrencySymbol))
stateController.update(AmountMaxValueStateTransformer(cryptoCurrencyStatus, yield))
}
override fun onCurrencyChangeClick(isFiat: Boolean) {
analyticsEventHandler.send(
StakingAnalyticsEvents.AmountSelectCurrency(cryptoCurrencyStatus.currency.symbol, isFiat),
StakingAnalyticsEvents.AmountSelectCurrency(value.cryptoCurrencySymbol, isFiat),
)
stateController.update(AmountCurrencyChangeStateTransformer(cryptoCurrencyStatus, isFiat))
}
@ -389,19 +390,25 @@ internal class StakingViewModel @Inject constructor(
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonValidator(
source = StakeScreenSource.Confirmation,
token = cryptoCurrencyStatus.currency.symbol,
token = value.cryptoCurrencySymbol,
),
)
stakingStateRouter.showValidators()
}
override fun onValidatorSelect(validator: Yield.Validator) {
analyticsEventHandler.send(
StakingAnalyticsEvents.ValidatorChosen(
value.cryptoCurrencySymbol,
validator.name,
),
)
stateController.update(ValidatorSelectChangeTransformer(validator))
}
override fun openRewardsValidators() {
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonRewards(value.cryptoCurrencyName),
StakingAnalyticsEvents.ButtonRewards(value.cryptoCurrencySymbol),
)
val rewardsValidators =
stateController.value.rewardsValidatorsState as? StakingStates.RewardsValidatorsState.Data
@ -412,7 +419,7 @@ internal class StakingViewModel @Inject constructor(
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonValidator(
source = StakeScreenSource.Info,
token = cryptoCurrencyStatus.currency.symbol,
token = value.cryptoCurrencySymbol,
),
)
onNextClick(actionTypeToOverwrite = StakingActionCommonType.PENDING_REWARDS)
@ -466,7 +473,7 @@ internal class StakingViewModel @Inject constructor(
analyticsEventHandler.send(
StakingAnalyticsEvents.ButtonValidator(
source = StakeScreenSource.Info,
token = cryptoCurrencyStatus.currency.symbol,
token = value.cryptoCurrencySymbol,
),
)
}
@ -511,7 +518,7 @@ internal class StakingViewModel @Inject constructor(
).fold(
ifLeft = { error ->
Timber.e(error.toString())
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencyName))
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol))
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
@ -533,7 +540,7 @@ internal class StakingViewModel @Inject constructor(
).fold(
ifLeft = { error ->
Timber.e(error.toString())
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencyName))
analyticsEventHandler.send(StakingAnalyticsEvents.TransactionError(value.cryptoCurrencySymbol))
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },

View file

@ -89,7 +89,7 @@ markdownComposeView = "0.5.4"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-797"
tangemBlockchainSdk = "develop-799"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-385"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^