Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-24 13:07:37 +03:00
parent 5b3a6d51b3
commit 950dc41662
10 changed files with 417 additions and 0 deletions

View file

@ -34,6 +34,7 @@ dependencies {
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.card)
implementation(projects.domain.demo)
implementation(projects.domain.legacy)
implementation(projects.domain.models)

View file

@ -0,0 +1,53 @@
package com.tangem.features.onramp.hottokens.portfolio
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.features.onramp.hottokens.portfolio.model.OnrampAddToPortfolioModel
import com.tangem.features.onramp.hottokens.portfolio.ui.OnrampAddToPortfolioBottomSheet
import com.tangem.features.onramp.hottokens.portfolio.ui.OnrampAddToPortfolioContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultOnrampAddToPortfolioComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: OnrampAddToPortfolioComponent.Params,
) : OnrampAddToPortfolioComponent, AppComponentContext by context {
private val model: OnrampAddToPortfolioModel = getOrCreateModel(params)
override fun dismiss() {
params.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.state.collectAsStateWithLifecycle()
val bottomSheetConfig = remember(key1 = this) {
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
)
}
OnrampAddToPortfolioBottomSheet(
config = bottomSheetConfig,
content = { OnrampAddToPortfolioContent(state = state) },
)
}
@AssistedFactory
interface Factory : OnrampAddToPortfolioComponent.Factory {
override fun create(
context: AppComponentContext,
params: OnrampAddToPortfolioComponent.Params,
): DefaultOnrampAddToPortfolioComponent
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.onramp.hottokens.portfolio
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
/**
* Onramp bottom sheet component for adding token to portfolio
*
[REDACTED_AUTHOR]
*/
interface OnrampAddToPortfolioComponent : ComposableBottomSheetComponent {
/**
* Params
*
* @property userWalletId user wallet id
* @property cryptoCurrency crypto currency
* @property currencyIconState currency icon state
* @property onSuccessAdding lambda be invoked when token will added
* @property onDismiss lambda be invoked when bottom sheet is dismissed
*/
data class Params(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val currencyIconState: CurrencyIconState,
val onSuccessAdding: () -> Unit,
val onDismiss: () -> Unit,
)
interface Factory : ComponentFactory<Params, OnrampAddToPortfolioComponent>
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.onramp.hottokens.portfolio.di
import com.tangem.features.onramp.hottokens.portfolio.DefaultOnrampAddToPortfolioComponent
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddToPortfolioComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface OnrampAddToPortfolioComponentModule {
@Binds
@Singleton
fun bindOnrampAddToPortfolioComponentFactory(
factory: DefaultOnrampAddToPortfolioComponent.Factory,
): OnrampAddToPortfolioComponent.Factory
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.onramp.hottokens.portfolio.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.onramp.hottokens.portfolio.model.OnrampAddToPortfolioModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
internal interface OnrampAddToPortfolioModelModule {
@Binds
@IntoMap
@ClassKey(OnrampAddToPortfolioModel::class)
fun bindOnrampSelectCountryModel(model: OnrampAddToPortfolioModel): Model
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.onramp.hottokens.portfolio.entity
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.domain.tokens.model.CryptoCurrency
internal sealed class OnrampAddToPortfolioBSConfig {
data class AddToPortfolio(
val cryptoCurrency: CryptoCurrency,
val currencyIconState: CurrencyIconState,
val onSuccessAdding: () -> Unit,
) : OnrampAddToPortfolioBSConfig()
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.onramp.hottokens.portfolio.entity
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.onramp.impl.R
/**
* Onramp add to portfolio UM
*
* @property currencyName currency name
* @property networkName network name
* @property currencyIconState currency icon state
* @property onAddClick lambda be invoked when add button is clicked
*
[REDACTED_AUTHOR]
*/
data class OnrampAddToPortfolioUM(
val currencyName: String,
val networkName: String,
val currencyIconState: CurrencyIconState,
val onAddClick: () -> Unit,
) {
val bsTitle: TextReference = resourceReference(id = R.string.common_add_token)
val subtitle: TextReference = resourceReference(
id = R.string.hot_crypto_token_network,
formatArgs = wrappedList(networkName),
)
val addButtonText: TextReference = resourceReference(R.string.common_add_to_portfolio)
}

View file

@ -0,0 +1,63 @@
package com.tangem.features.onramp.hottokens.portfolio.model
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.features.onramp.hottokens.portfolio.OnrampAddToPortfolioComponent
import com.tangem.features.onramp.hottokens.portfolio.entity.OnrampAddToPortfolioUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
/**
* Model for adding token to portfolio
*
* @param paramsContainer params container
* @property dispatchers dispatchers
* @property derivePublicKeysUseCase use case for deriving public key
* @property addCryptoCurrenciesUseCase use case for adding crypto currency
*
[REDACTED_AUTHOR]
*/
@ComponentScoped
internal class OnrampAddToPortfolioModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
) : Model() {
val state: StateFlow<OnrampAddToPortfolioUM> get() = _state
private val _state = MutableStateFlow(value = getInitialState())
private val params: OnrampAddToPortfolioComponent.Params = paramsContainer.require()
private fun getInitialState(): OnrampAddToPortfolioUM {
return OnrampAddToPortfolioUM(
currencyName = params.cryptoCurrency.name,
networkName = params.cryptoCurrency.network.name,
currencyIconState = params.currencyIconState,
onAddClick = ::onAddClick,
)
}
private fun onAddClick() {
modelScope.launch {
derivePublicKeysUseCase(params.userWalletId, listOfNotNull(params.cryptoCurrency)).getOrElse {
Timber.e("Failed to derive public keys: $it")
}
addCryptoCurrenciesUseCase(
userWalletId = params.userWalletId,
currencies = listOfNotNull(params.cryptoCurrency),
)
.onRight { params.onSuccessAdding() }
}
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.onramp.hottokens.portfolio.ui
import androidx.compose.runtime.Composable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun OnrampAddToPortfolioBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = config,
addBottomInsets = true,
containerColor = TangemTheme.colors.background.tertiary,
content = { content() },
)
}

View file

@ -0,0 +1,162 @@
package com.tangem.features.onramp.hottokens.portfolio.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onramp.hottokens.portfolio.entity.OnrampAddToPortfolioUM
import com.tangem.features.onramp.impl.R
/**
* Onramp add to portfolio content
*
* @param state state
*/
@Composable
fun OnrampAddToPortfolioContent(state: OnrampAddToPortfolioUM) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
TopAppBar(text = state.bsTitle)
SpacerH(height = 19.dp)
CurrencyIcon(state = state.currencyIconState)
SpacerH12()
CurrencyName(
text = state.currencyName,
modifier = Modifier.padding(horizontal = 16.dp),
)
SpacerH(height = 6.dp)
CurrencyNetworkName(
text = state.subtitle,
modifier = Modifier.padding(horizontal = 16.dp),
)
SpacerH24()
AddToPortfolioButton(
text = state.addButtonText,
onClick = state.onAddClick,
modifier = Modifier.padding(horizontal = 16.dp),
)
SpacerH16()
}
}
@Composable
private fun TopAppBar(text: TextReference) {
TangemTopAppBar(
title = text.resolveReference(),
endButton = TopAppBarButtonUM(
iconRes = R.drawable.ic_information_24,
onIconClicked = {},
),
iconTint = TangemTheme.colors.icon.informative,
titleAlignment = Alignment.CenterHorizontally,
height = TangemTopAppBarHeight.BOTTOM_SHEET,
)
}
@Composable
private fun CurrencyName(text: String, modifier: Modifier = Modifier) {
Text(
text = text,
modifier = modifier,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.subtitle1,
)
}
@Composable
private fun CurrencyNetworkName(text: TextReference, modifier: Modifier = Modifier) {
Text(
text = text.resolveReference(),
modifier = modifier,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
style = TangemTheme.typography.body2,
)
}
@Composable
private fun AddToPortfolioButton(text: TextReference, onClick: () -> Unit, modifier: Modifier = Modifier) {
TangemButton(
text = text.resolveReference(),
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
onClick = onClick,
colors = TangemButtonsDefaults.primaryButtonColors,
showProgress = false,
enabled = true,
modifier = modifier.fillMaxWidth(),
size = TangemButtonSize.WideAction,
textStyle = TangemTheme.typography.subtitle1,
)
}
@Preview
@Composable
private fun PreviewOnrampAddToPortfolioContent() {
TangemThemePreview {
val bottomSheetConfig = remember {
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
)
}
OnrampAddToPortfolioBottomSheet(
config = bottomSheetConfig,
content = {
OnrampAddToPortfolioContent(
state = OnrampAddToPortfolioUM(
currencyName = "Tether",
networkName = "Ethereum",
currencyIconState = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_eth_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
showCustomBadge = false,
),
onAddClick = {},
),
)
},
)
}
}