Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-30 13:07:15 +04:00
parent 3cf222aa32
commit a99b337270
13 changed files with 296 additions and 24 deletions

View file

@ -12,7 +12,7 @@ internal interface CustomTokenFormComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val network: SelectedNetwork,
val derivationPath: SelectedDerivationPath,
val derivationPath: SelectedDerivationPath?,
val formValues: CustomTokenFormValues,
val onSelectNetworkClick: (CustomTokenFormValues) -> Unit,
val onSelectDerivationPathClick: (CustomTokenFormValues) -> Unit,

View file

@ -18,6 +18,7 @@ internal interface CustomTokenSelectorComponent : ComposableContentComponent {
data class DerivationPathSelector(
val userWalletId: UserWalletId,
val selectedNetwork: SelectedNetwork,
val selectedDerivationPath: SelectedDerivationPath?,
val onDerivationPathSelected: (SelectedDerivationPath) -> Unit,
) : Params()

View file

@ -12,7 +12,6 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.CustomTokenFormComponent
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
@ -20,7 +19,6 @@ import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig
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.impl.R
import com.tangem.features.managetokens.ui.AddCustomTokenBottomSheet
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -110,6 +108,9 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
context = childByContext(componentContext),
params = CustomTokenSelectorComponent.Params.DerivationPathSelector(
userWalletId = config.userWalletId,
selectedNetwork = requireNotNull(config.selectedNetwork) {
"Network is not selected"
},
selectedDerivationPath = config.selectedDerivationPath,
onDerivationPathSelected = { derivationPath ->
showForm(derivationPath = derivationPath)
@ -122,12 +123,10 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
context = childByContext(componentContext),
params = CustomTokenFormComponent.Params(
userWalletId = config.userWalletId,
network = config.selectedNetwork ?: error("Network is not selected"),
derivationPath = config.selectedDerivationPath ?: SelectedDerivationPath(
id = config.selectedNetwork.id,
value = config.selectedNetwork.derivationPath,
networkName = resourceReference(R.string.custom_token_derivation_path_default),
),
network = requireNotNull(config.selectedNetwork) {
"Network is not selected"
},
derivationPath = config.selectedDerivationPath,
formValues = config.formValues,
onSelectNetworkClick = ::showNetworkSelector,
onSelectDerivationPathClick = ::showDerivationPathSelector,

View file

@ -4,33 +4,61 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableDialogComponent
import com.tangem.features.managetokens.component.CustomTokenDerivationInputComponent
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorDialogConfig
import com.tangem.features.managetokens.model.CustomTokenSelectorModel
import com.tangem.features.managetokens.ui.CustomTokenSelectorContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
internal class DefaultCustomTokenSelectorComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: CustomTokenSelectorComponent.Params,
private val customTokenDerivationInputComponentFactory: CustomTokenDerivationInputComponent.Factory,
) : CustomTokenSelectorComponent, AppComponentContext by context {
private val state: MutableStateFlow<CustomTokenSelectorUM> = MutableStateFlow(
value = PreviewCustomTokenSelectorComponent(params = params).previewState,
private val model: CustomTokenSelectorModel = getOrCreateModel(params)
private val dialogSlot = childSlot(
source = model.dialogNavigation,
serializer = CustomTokenSelectorDialogConfig.serializer(),
childFactory = ::createDialog,
)
private fun createDialog(
config: CustomTokenSelectorDialogConfig,
context: ComponentContext,
): ComposableDialogComponent = when (config) {
is CustomTokenSelectorDialogConfig.CustomDerivationInput -> customTokenDerivationInputComponentFactory.create(
context = childByContext(context),
params = CustomTokenDerivationInputComponent.Params(
userWalletId = config.userWalletId,
onConfirm = model::selectCustomDerivationPath,
onDismiss = model.dialogNavigation::dismiss,
),
)
}
@Composable
override fun Content(modifier: Modifier) {
val state by state.collectAsStateWithLifecycle()
val state by model.state.collectAsStateWithLifecycle()
val dialog by dialogSlot.subscribeAsState()
CustomTokenSelectorContent(
modifier = modifier,
model = state,
)
dialog.child?.instance?.Dialog()
}
@AssistedFactory

View file

@ -60,7 +60,8 @@ internal class PreviewAddCustomTokenComponent(
PreviewCustomTokenSelectorComponent(
params = CustomTokenSelectorComponent.Params.DerivationPathSelector(
userWalletId = state.userWalletId,
selectedDerivationPath = state.selectedDerivationPath,
selectedNetwork = state.selectedNetwork!!,
selectedDerivationPath = state.selectedDerivationPath!!,
onDerivationPathSelected = {},
),
).Content(modifier)

View file

@ -35,7 +35,7 @@ internal class PreviewCustomTokenSelectorComponent(
)
DerivationPathUM(
id = d.id.value,
id = d.id?.value ?: "",
value = d.value.value.orEmpty(),
networkName = d.networkName,
isSelected = d.value == params.selectedDerivationPath?.value,
@ -75,7 +75,10 @@ internal class PreviewCustomTokenSelectorComponent(
val previewState = CustomTokenSelectorUM(
header = when (params) {
is Params.DerivationPathSelector -> CustomTokenSelectorUM.HeaderUM.CustomDerivationButton({})
is Params.DerivationPathSelector -> CustomTokenSelectorUM.HeaderUM.CustomDerivationButton(
value = null,
onClick = {},
)
is Params.NetworkSelector -> if (params.selectedNetwork == null) {
CustomTokenSelectorUM.HeaderUM.Description
} else {

View file

@ -2,6 +2,8 @@ package com.tangem.features.managetokens.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.managetokens.model.CustomTokenFormModel
import com.tangem.features.managetokens.model.CustomTokenSelectorModel
import com.tangem.features.managetokens.model.ManageTokensModel
import dagger.Binds
import dagger.Module
@ -17,4 +19,14 @@ internal interface ModelModule {
@IntoMap
@ClassKey(ManageTokensModel::class)
fun provideManageTokensModel(model: ManageTokensModel): Model
@Binds
@IntoMap
@ClassKey(CustomTokenFormModel::class)
fun provideCustomTokenFormModel(model: CustomTokenFormModel): Model
@Binds
@IntoMap
@ClassKey(CustomTokenSelectorModel::class)
fun provideCustomTokenSelectorModel(model: CustomTokenSelectorModel): Model
}

View file

@ -34,7 +34,7 @@ internal data class SelectedNetwork(
@Serializable
internal data class SelectedDerivationPath(
val id: Network.ID,
val id: Network.ID?,
val value: Network.DerivationPath,
val networkName: TextReference,
)

View file

@ -17,6 +17,7 @@ internal data class CustomTokenSelectorUM(
data object Description : HeaderUM()
data class CustomDerivationButton(
val value: String?,
val onClick: () -> Unit,
) : HeaderUM()
}

View file

@ -14,6 +14,7 @@ import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.features.managetokens.component.CustomTokenFormComponent
import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM
@ -57,7 +58,7 @@ internal class CustomTokenFormModel @Inject constructor(
customCurrencyValidator.createCoin(
userWalletId = params.userWalletId,
networkId = params.network.id,
derivationPath = params.derivationPath.value,
derivationPath = getDerivationPath(),
)
}
}
@ -77,7 +78,7 @@ internal class CustomTokenFormModel @Inject constructor(
},
derivationPath = ClickableFieldUM(
label = resourceReference(R.string.custom_token_derivation_path),
value = if (params.derivationPath.id == params.network.id) {
value = if (params.derivationPath == null || params.derivationPath.id == params.network.id) {
resourceReference(R.string.custom_token_derivation_path_default)
} else {
params.derivationPath.networkName
@ -104,7 +105,7 @@ internal class CustomTokenFormModel @Inject constructor(
customCurrencyValidator.validateForm(
userWalletId = params.userWalletId,
networkId = params.network.id,
derivationPath = params.derivationPath.value,
derivationPath = getDerivationPath(),
formValues = formValues,
)
}
@ -266,6 +267,10 @@ internal class CustomTokenFormModel @Inject constructor(
return formValues.fillValues(form)
}
private fun getDerivationPath(): Network.DerivationPath {
return params.derivationPath?.value ?: params.network.derivationPath
}
private fun updateContractAddress(value: String) {
state.update { state ->
state.updateTokenForm {

View file

@ -0,0 +1,172 @@
package com.tangem.features.managetokens.model
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.GetSupportedNetworksUseCase
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params.DerivationPathSelector
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params.NetworkSelector
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorDialogConfig
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM.HeaderUM
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
import com.tangem.features.managetokens.entity.item.DerivationPathUM
import com.tangem.features.managetokens.entity.item.SelectableItemUM
import com.tangem.features.managetokens.impl.R
import com.tangem.features.managetokens.utils.mapper.toCurrencyNetworkModel
import com.tangem.features.managetokens.utils.mapper.toDerivationPathModel
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ComponentScoped
internal class CustomTokenSelectorModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getSupportedNetworksUseCase: GetSupportedNetworksUseCase,
private val messageSender: UiMessageSender,
paramsContainer: ParamsContainer,
) : Model() {
private val params: CustomTokenSelectorComponent.Params = paramsContainer.require()
val dialogNavigation: SlotNavigation<CustomTokenSelectorDialogConfig> = SlotNavigation()
val state: MutableStateFlow<CustomTokenSelectorUM> = MutableStateFlow(
value = getInitialState(),
)
init {
loadItems()
}
private fun getInitialState(): CustomTokenSelectorUM = when (params) {
is NetworkSelector -> CustomTokenSelectorUM(
header = if (params.selectedNetwork == null) {
HeaderUM.Description
} else {
HeaderUM.None
},
items = persistentListOf(),
)
is DerivationPathSelector -> CustomTokenSelectorUM(
header = HeaderUM.CustomDerivationButton(
value = (params.selectedDerivationPath?.value as? Network.DerivationPath.Custom)?.value,
onClick = ::showCustomDerivationInput,
),
items = persistentListOf(),
)
}
private fun loadItems() = modelScope.launch {
val items = when (params) {
is NetworkSelector -> loadNetworks(params).toImmutableList()
is DerivationPathSelector -> loadDerivationPaths(params).toImmutableList()
}
state.update { state ->
state.copy(items = items)
}
}
private suspend fun loadNetworks(selector: NetworkSelector): List<SelectableItemUM> {
return getSupportedNetworks(selector.userWalletId).map { network ->
network.toCurrencyNetworkModel(
isSelected = network.id == selector.selectedNetwork?.id,
onSelectedStateChange = {
val model = SelectedNetwork(
id = network.id,
name = stringReference(network.name),
derivationPath = network.derivationPath,
canHandleTokens = network.canHandleTokens,
)
selector.onNetworkSelected(model)
},
)
}
}
private suspend fun loadDerivationPaths(selector: DerivationPathSelector): List<SelectableItemUM> {
val derivationPaths = mutableListOf<DerivationPathUM>()
val defaultPath = selector.selectedNetwork.let { network ->
network.toDerivationPathModel(
isSelected = network.id == selector.selectedDerivationPath?.id,
onSelectedStateChange = {
val model = SelectedDerivationPath(
id = network.id,
networkName = resourceReference(R.string.custom_token_derivation_path_default),
value = network.derivationPath,
)
selector.onDerivationPathSelected(model)
},
)
}
if (defaultPath != null) {
derivationPaths.add(defaultPath)
}
getSupportedNetworks(selector.userWalletId)
.mapNotNullTo(derivationPaths) { network ->
if (network.id == selector.selectedNetwork.id) {
return@mapNotNullTo null // Skip default path
}
network.toDerivationPathModel(
isSelected = network.id == selector.selectedDerivationPath?.id,
onSelectedStateChange = {
val model = SelectedDerivationPath(
id = network.id,
networkName = stringReference(network.name),
value = network.derivationPath,
)
selector.onDerivationPathSelected(model)
},
)
}
return derivationPaths
}
private suspend fun getSupportedNetworks(userWalletId: UserWalletId): List<Network> {
return getSupportedNetworksUseCase(userWalletId).getOrElse { e ->
val message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error))
messageSender.send(message)
emptyList()
}
}
private fun showCustomDerivationInput() {
val config = when (params) {
is NetworkSelector -> return
is DerivationPathSelector -> CustomTokenSelectorDialogConfig.CustomDerivationInput(params.userWalletId)
}
dialogNavigation.activate(config)
}
fun selectCustomDerivationPath(value: SelectedDerivationPath) {
when (params) {
is NetworkSelector -> return
is DerivationPathSelector -> params.onDerivationPathSelected(value)
}
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.managetokens.utils.mapper
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork
import com.tangem.domain.tokens.model.Network
import com.tangem.features.managetokens.entity.item.CurrencyItemUM.Basic.NetworksUM
import com.tangem.features.managetokens.entity.item.CurrencyNetworkUM
import com.tangem.features.managetokens.utils.ui.getIconRes
@ -15,7 +16,7 @@ internal fun ManagedCryptoCurrency.Token.toUiNetworksModel(
return if (isExpanded) {
NetworksUM.Expanded(
networks = availableNetworks.map {
it.toUiModel(
it.toCurrencyNetworkModel(
isSelected = it.network in addedIn,
isEditable = isItemsEditable,
onSelectedStateChange = onSelectedStateChange,
@ -27,7 +28,22 @@ internal fun ManagedCryptoCurrency.Token.toUiNetworksModel(
}
}
private fun SourceNetwork.toUiModel(
internal fun Network.toCurrencyNetworkModel(
isSelected: Boolean,
onSelectedStateChange: (Boolean) -> Unit,
): CurrencyNetworkUM {
return CurrencyNetworkUM(
network = this,
name = name,
iconResId = id.getIconRes(isColored = true),
isSelected = isSelected,
type = standardType.name,
isMainNetwork = false,
onSelectedStateChange = onSelectedStateChange,
)
}
private fun SourceNetwork.toCurrencyNetworkModel(
isSelected: Boolean,
isEditable: Boolean,
onSelectedStateChange: (SourceNetwork, Boolean) -> Unit,

View file

@ -0,0 +1,34 @@
package com.tangem.features.managetokens.utils.mapper
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.model.Network
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
import com.tangem.features.managetokens.entity.item.DerivationPathUM
import com.tangem.features.managetokens.impl.R
internal fun Network.toDerivationPathModel(
isSelected: Boolean,
onSelectedStateChange: (Boolean) -> Unit,
): DerivationPathUM? {
return DerivationPathUM(
id = id.value,
value = derivationPath.value ?: return null,
networkName = stringReference(name),
isSelected = isSelected,
onSelectedStateChange = onSelectedStateChange,
)
}
internal fun SelectedNetwork.toDerivationPathModel(
isSelected: Boolean,
onSelectedStateChange: (Boolean) -> Unit,
): DerivationPathUM? {
return DerivationPathUM(
id = id.value,
value = derivationPath.value ?: return null,
networkName = resourceReference(R.string.custom_token_derivation_path_default),
isSelected = isSelected,
onSelectedStateChange = onSelectedStateChange,
)
}