Updated on 2026-08-14
This commit is contained in:
commit
d2d9669097
12 changed files with 431 additions and 3 deletions
|
|
@ -46,7 +46,7 @@ inline fun RowContentContainer(
|
|||
}
|
||||
|
||||
@Composable
|
||||
internal fun RowText(
|
||||
fun RowText(
|
||||
mainText: String,
|
||||
secondText: String,
|
||||
accentMainText: Boolean,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ dependencies {
|
|||
/** Feature Apis */
|
||||
implementation(projects.features.tester.api)
|
||||
|
||||
/* SDK */
|
||||
implementation(deps.tangem.blockchain)
|
||||
|
||||
/** Other modules */
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.libs.crypto)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package com.tangem.feature.tester.presentation
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
|
|
@ -17,6 +19,8 @@ import com.tangem.feature.tester.presentation.actions.TesterActionsScreen
|
|||
import com.tangem.feature.tester.presentation.actions.TesterActionsViewModel
|
||||
import com.tangem.feature.tester.presentation.environments.ui.EnvironmentTogglesScreen
|
||||
import com.tangem.feature.tester.presentation.environments.viewmodels.EnvironmentsTogglesViewModel
|
||||
import com.tangem.feature.tester.presentation.excludedblockchains.ExcludedBlockchainsScreen
|
||||
import com.tangem.feature.tester.presentation.excludedblockchains.ExcludedBlockchainsViewModel
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.ui.FeatureTogglesScreen
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.viewmodels.FeatureTogglesViewModel
|
||||
import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState
|
||||
|
|
@ -70,6 +74,9 @@ internal class TesterActivity : ComposeActivity() {
|
|||
onBackClick = { appRouter.pop { finish() } },
|
||||
onFeatureTogglesClick = { innerTesterRouter.open(TesterScreen.FEATURE_TOGGLES) },
|
||||
onEnvironmentTogglesClick = { innerTesterRouter.open(TesterScreen.ENVIRONMENTS_TOGGLES) },
|
||||
onExcludedBlockchainsClick = {
|
||||
innerTesterRouter.open(TesterScreen.EXCLUDED_BLOCKCHAINS)
|
||||
},
|
||||
onTesterActionsClick = { innerTesterRouter.open(TesterScreen.TESTER_ACTIONS) },
|
||||
),
|
||||
)
|
||||
|
|
@ -100,6 +107,15 @@ internal class TesterActivity : ComposeActivity() {
|
|||
|
||||
TesterActionsScreen(state = viewModel.uiState)
|
||||
}
|
||||
|
||||
composable(route = TesterScreen.EXCLUDED_BLOCKCHAINS.name) {
|
||||
val viewModel = hiltViewModel<ExcludedBlockchainsViewModel>().apply {
|
||||
setupNavigation(innerTesterRouter, appFinisher)
|
||||
}
|
||||
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
ExcludedBlockchainsScreen(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
package com.tangem.feature.tester.presentation.excludedblockchains
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.TangemSwitch
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.bottomFade
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.components.rows.RowContentContainer
|
||||
import com.tangem.core.ui.components.rows.RowText
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.feature.tester.impl.R
|
||||
import com.tangem.feature.tester.presentation.excludedblockchains.state.BlockchainUM
|
||||
import com.tangem.feature.tester.presentation.excludedblockchains.state.ExcludedBlockchainsScreenUM
|
||||
import com.tangem.feature.tester.presentation.excludedblockchains.state.mapper.toUiModels
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Composable
|
||||
internal fun ExcludedBlockchainsScreen(state: ExcludedBlockchainsScreenUM, modifier: Modifier = Modifier) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
topBar = {
|
||||
TangemTopAppBar(
|
||||
title = resourceReference(R.string.excluded_blockchains),
|
||||
startButton = TopAppBarButtonUM.Back(onBackClicked = state.popBack),
|
||||
endButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_refresh_24,
|
||||
onIconClicked = state.onRecoverClick,
|
||||
),
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
floatingActionButton = {
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
text = "Restart App",
|
||||
onClick = state.onRestartClick,
|
||||
)
|
||||
},
|
||||
content = { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.bottomFade()
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
top = 8.dp,
|
||||
bottom = 96.dp,
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
),
|
||||
) {
|
||||
item {
|
||||
SearchBar(
|
||||
modifier = Modifier.padding(bottom = 12.dp),
|
||||
state = state.search,
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
TogglesNotification(
|
||||
modifier = Modifier.padding(vertical = 12.dp),
|
||||
appVersion = state.appVersion,
|
||||
isVisible = state.showRecoverWarning,
|
||||
)
|
||||
}
|
||||
|
||||
items(
|
||||
items = state.blockchains,
|
||||
key = BlockchainUM::id,
|
||||
) { item ->
|
||||
BlockchainItem(
|
||||
model = item,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TogglesNotification(appVersion: String, isVisible: Boolean, modifier: Modifier = Modifier) {
|
||||
if (isVisible) {
|
||||
Notification(
|
||||
modifier = modifier,
|
||||
config = NotificationConfig(
|
||||
subtitle = resourceReference(
|
||||
id = R.string.feature_toggles_custom_setup_warning_description,
|
||||
wrappedList(appVersion),
|
||||
),
|
||||
iconResId = R.drawable.ic_alert_triangle_20,
|
||||
title = resourceReference(id = R.string.feature_toggles_custom_setup_warning_title),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BlockchainItem(model: BlockchainUM, modifier: Modifier = Modifier) {
|
||||
RowContentContainer(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = TangemTheme.dimens.size52),
|
||||
icon = {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(TangemTheme.dimens.size22),
|
||||
painter = painterResource(id = model.iconResId),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
RowText(
|
||||
mainText = model.name,
|
||||
secondText = model.symbol,
|
||||
accentMainText = false,
|
||||
accentSecondText = false,
|
||||
)
|
||||
},
|
||||
action = {
|
||||
TangemSwitch(
|
||||
checked = model.isExcluded,
|
||||
onCheckedChange = model.onExcludedStateChange,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_ExcludedBlockchainsScreen(
|
||||
@PreviewParameter(ExcludedBlockchainsScreenPreviewProvider::class) params: ExcludedBlockchainsScreenUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
ExcludedBlockchainsScreen(params)
|
||||
}
|
||||
}
|
||||
|
||||
private class ExcludedBlockchainsScreenPreviewProvider : PreviewParameterProvider<ExcludedBlockchainsScreenUM> {
|
||||
override val values: Sequence<ExcludedBlockchainsScreenUM>
|
||||
get() = sequenceOf(
|
||||
ExcludedBlockchainsScreenUM(
|
||||
blockchains = Blockchain.entries.toUiModels(
|
||||
excludedBlockchainsIds = setOf(Blockchain.Ethereum.id, Blockchain.Solana.id),
|
||||
onExcludedStateChange = { _, _ -> },
|
||||
).toPersistentList(),
|
||||
showRecoverWarning = true,
|
||||
appVersion = "1.0.0",
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.excluded_blockchains_search_placeholder),
|
||||
query = "",
|
||||
isActive = false,
|
||||
onQueryChange = {},
|
||||
onActiveChange = {},
|
||||
),
|
||||
popBack = {},
|
||||
onRecoverClick = {},
|
||||
onRestartClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package com.tangem.feature.tester.presentation.excludedblockchains
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
|
||||
import com.tangem.core.navigation.finisher.AppFinisher
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.feature.tester.impl.R
|
||||
import com.tangem.feature.tester.presentation.excludedblockchains.state.BlockchainUM
|
||||
import com.tangem.feature.tester.presentation.excludedblockchains.state.ExcludedBlockchainsScreenUM
|
||||
import com.tangem.feature.tester.presentation.excludedblockchains.state.mapper.toUiModels
|
||||
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.mutate
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class ExcludedBlockchainsViewModel @Inject constructor(
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
excludedBlockchainsManager: MutableExcludedBlockchainsManager?,
|
||||
) : ViewModel() {
|
||||
|
||||
private val excludedBlockchainsManager: MutableExcludedBlockchainsManager =
|
||||
requireNotNull(excludedBlockchainsManager) {
|
||||
"Mutable excluded blockchains manager can't be null when tester actions is available"
|
||||
}
|
||||
|
||||
val state: MutableStateFlow<ExcludedBlockchainsScreenUM> = MutableStateFlow(
|
||||
value = getInitialState(),
|
||||
)
|
||||
|
||||
init {
|
||||
observeQueryUpdates()
|
||||
}
|
||||
|
||||
fun setupNavigation(router: InnerTesterRouter, appFinisher: AppFinisher) {
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
popBack = router::back,
|
||||
onRestartClick = appFinisher::finish,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialState(): ExcludedBlockchainsScreenUM = ExcludedBlockchainsScreenUM(
|
||||
popBack = {},
|
||||
search = getInitialSearchBar(),
|
||||
blockchains = getBlockchains(),
|
||||
showRecoverWarning = !excludedBlockchainsManager.isMatchLocalConfig(),
|
||||
appVersion = appVersionProvider.versionName,
|
||||
onRestartClick = {},
|
||||
onRecoverClick = ::recoverLocalConfig,
|
||||
)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun observeQueryUpdates() {
|
||||
state
|
||||
.map { it.search.query }
|
||||
.distinctUntilChanged()
|
||||
.drop(count = 1) // Skip initial value
|
||||
.sample(periodMillis = 1_000)
|
||||
.onEach(::filterBlockchains)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun recoverLocalConfig() = viewModelScope.launch {
|
||||
excludedBlockchainsManager.recoverLocalConfig()
|
||||
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
search = getInitialSearchBar(),
|
||||
blockchains = getBlockchains(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockchains(): PersistentList<BlockchainUM> {
|
||||
val excludedBlockchains = excludedBlockchainsManager.excludedBlockchainsIds
|
||||
|
||||
return Blockchain.entries.toUiModels(
|
||||
excludedBlockchainsIds = excludedBlockchains,
|
||||
onExcludedStateChange = ::excludeBlockchain,
|
||||
).toPersistentList()
|
||||
}
|
||||
|
||||
private fun getInitialSearchBar(): SearchBarUM = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.excluded_blockchains_search_placeholder),
|
||||
query = "",
|
||||
isActive = false,
|
||||
onQueryChange = ::updateQuery,
|
||||
onActiveChange = ::updateSearchBarState,
|
||||
)
|
||||
|
||||
private fun excludeBlockchain(blockchain: Blockchain, isExcluded: Boolean) = viewModelScope.launch {
|
||||
excludedBlockchainsManager.excludeBlockchain(blockchain.id, isExcluded)
|
||||
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
blockchains = state.blockchains.mutate { mutableBlockchains ->
|
||||
val index = mutableBlockchains.indexOfFirst { it.id == blockchain.id }
|
||||
if (index == -1) return@mutate
|
||||
|
||||
mutableBlockchains[index] = mutableBlockchains[index].copy(isExcluded = isExcluded)
|
||||
},
|
||||
showRecoverWarning = !excludedBlockchainsManager.isMatchLocalConfig(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterBlockchains(query: String) {
|
||||
val filteredBlockchains = if (query.isBlank()) {
|
||||
getBlockchains()
|
||||
} else {
|
||||
getBlockchains().filter { blockchain ->
|
||||
blockchain.name.contains(query, ignoreCase = true) ||
|
||||
blockchain.symbol.contains(query, ignoreCase = true)
|
||||
}.toPersistentList()
|
||||
}
|
||||
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
blockchains = filteredBlockchains,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateQuery(query: String) {
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
search = state.search.copy(query = query),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSearchBarState(isActive: Boolean) {
|
||||
state.update { state ->
|
||||
state.copy(
|
||||
search = state.search.copy(isActive = isActive),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.feature.tester.presentation.excludedblockchains.state
|
||||
|
||||
internal data class BlockchainUM(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val iconResId: Int,
|
||||
val isExcluded: Boolean,
|
||||
val onExcludedStateChange: (isExcluded: Boolean) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.feature.tester.presentation.excludedblockchains.state
|
||||
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
internal data class ExcludedBlockchainsScreenUM(
|
||||
val popBack: () -> Unit,
|
||||
val search: SearchBarUM,
|
||||
val blockchains: PersistentList<BlockchainUM>,
|
||||
val showRecoverWarning: Boolean,
|
||||
val appVersion: String,
|
||||
val onRecoverClick: () -> Unit,
|
||||
val onRestartClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.feature.tester.presentation.excludedblockchains.state.mapper
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.feature.tester.presentation.excludedblockchains.state.BlockchainUM
|
||||
|
||||
internal fun List<Blockchain>.toUiModels(
|
||||
excludedBlockchainsIds: Set<String>,
|
||||
onExcludedStateChange: (Blockchain, Boolean) -> Unit,
|
||||
): List<BlockchainUM> = mapNotNull { blockchain ->
|
||||
blockchain
|
||||
.takeUnless { it == Blockchain.Unknown || it.isTestnet() }
|
||||
?.toUiModel(
|
||||
isExcluded = blockchain.id in excludedBlockchainsIds,
|
||||
onExcludedStateChange = { isExcluded ->
|
||||
onExcludedStateChange(blockchain, isExcluded)
|
||||
},
|
||||
)
|
||||
}
|
||||
.sortedBy(BlockchainUM::name)
|
||||
.sortedByDescending(BlockchainUM::isExcluded)
|
||||
|
||||
private fun Blockchain.toUiModel(isExcluded: Boolean, onExcludedStateChange: (Boolean) -> Unit): BlockchainUM =
|
||||
BlockchainUM(
|
||||
id = id,
|
||||
name = name,
|
||||
symbol = currency,
|
||||
iconResId = getActiveIconRes(id),
|
||||
isExcluded = isExcluded,
|
||||
onExcludedStateChange = onExcludedStateChange,
|
||||
)
|
||||
|
|
@ -12,5 +12,6 @@ data class TesterMenuContentState(
|
|||
val onBackClick: () -> Unit,
|
||||
val onFeatureTogglesClick: () -> Unit,
|
||||
val onEnvironmentTogglesClick: () -> Unit,
|
||||
val onExcludedBlockchainsClick: () -> Unit,
|
||||
val onTesterActionsClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -51,6 +51,12 @@ internal fun TesterMenuScreen(state: TesterMenuContentState) {
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.excluded_blockchains),
|
||||
onClick = state.onExcludedBlockchainsClick,
|
||||
)
|
||||
|
||||
PrimaryButton(
|
||||
text = stringResource(R.string.environment_toggles),
|
||||
onClick = state.onEnvironmentTogglesClick,
|
||||
|
|
@ -76,6 +82,7 @@ private fun PreviewTesterMenuScreen() {
|
|||
onBackClick = {},
|
||||
onFeatureTogglesClick = {},
|
||||
onEnvironmentTogglesClick = {},
|
||||
onExcludedBlockchainsClick = {},
|
||||
onTesterActionsClick = {},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,5 +6,9 @@ package com.tangem.feature.tester.presentation.navigation
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal enum class TesterScreen {
|
||||
MENU, FEATURE_TOGGLES, ENVIRONMENTS_TOGGLES, TESTER_ACTIONS
|
||||
MENU,
|
||||
FEATURE_TOGGLES,
|
||||
ENVIRONMENTS_TOGGLES,
|
||||
TESTER_ACTIONS,
|
||||
EXCLUDED_BLOCKCHAINS,
|
||||
}
|
||||
|
|
@ -3,10 +3,12 @@
|
|||
<string name="tester_menu" translatable="false">Tester menu</string>
|
||||
<string name="feature_toggles" translatable="false">Feature toggles</string>
|
||||
<string name="feature_toggles_custom_setup_warning_title" translatable="false">Custom setup</string>
|
||||
<string name="feature_toggles_custom_setup_warning_description" translatable="false">Feature toggles differs from v%s config</string>
|
||||
<string name="feature_toggles_custom_setup_warning_description" translatable="false">Toggles differs from v%s config</string>
|
||||
<string name="restart_app" translatable="false">Restart app</string>
|
||||
<string name="environment_toggles" translatable="false">Environment toggles</string>
|
||||
<string name="tester_actions" translatable="false">Tester actions</string>
|
||||
<string name="hide_all_currencies" translatable="false">Hide all currencies</string>
|
||||
<string name="toggle_app_theme" translatable="false">Toggle app theme - %s</string>
|
||||
<string name="excluded_blockchains" translatable="false">Excluded blockchains</string>
|
||||
<string name="excluded_blockchains_search_placeholder" translatable="false">Filter by name or symbol</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue