Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-01 15:44:36 +05:00
parent a1a82d3ece
commit b27ff31652
52 changed files with 860 additions and 518 deletions

View file

@ -1,93 +1,232 @@
package com.tangem.features.commonfeatures.impl.addfunds
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.LocalTangemBottomSheetContentBottomInset
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.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemeRedesign
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.addfunds.model.AddFundsModel
import com.tangem.features.commonfeatures.impl.addtoportfolio.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.addfunds.model.uiSpec
import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.userportfolio.UserPortfolioComponent
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import com.tangem.core.ui.R as CoreR
@Suppress("LongParameterList")
internal class DefaultAddFundsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: AddFundsComponent.Params,
chooseTokenComponentFactory: ChooseTokenComponent.Factory,
tokenActionsComponentFactory: TokenActionsComponent.Factory,
userPortfolioComponentFactory: UserPortfolioComponent.Factory,
walletFeatureToggles: WalletFeatureToggles,
) : AppComponentContext by appComponentContext, AddFundsComponent {
private val model: AddFundsModel = getOrCreateModel(params)
private val chooseTokenComponent: ChooseTokenComponent = chooseTokenComponentFactory.create(
context = child(key = "addFundsChooseToken"),
params = ChooseTokenComponent.Params(bridge = model.chooseTokenBridge),
)
private val isCompactTokenActions: Boolean = params.launchMode is AddFundsComponent.LaunchMode.TokenActionsOnly
private val tokenActionsComponent: TokenActionsComponent = tokenActionsComponentFactory.create(
context = child(key = "addFundsTokenActions"),
params = TokenActionsComponent.Params(
data = model.tokenActionsData,
callbacks = model,
bottomAction = TokenActionsComponent.BottomAction.GoToToken,
isRedesignForced = true,
),
)
private val isAddFundsStage1Enabled: Boolean = walletFeatureToggles.isAddFundsStage1Enabled
private val tokenActionsComponent: TokenActionsComponent by lazy {
tokenActionsComponentFactory.create(
context = child(key = "addFundsTokenActions"),
params = TokenActionsComponent.Params(
data = model.tokenActionsData,
callbacks = model,
bottomAction = model.currentBottomAction,
isRedesignForced = true,
isCompact = isCompactTokenActions,
),
)
}
private val chooseTokenComponent: ChooseTokenComponent? by lazy {
(params.launchMode as? AddFundsComponent.LaunchMode.ChooseToken)?.let {
chooseTokenComponentFactory.create(
context = child(key = "addFundsChooseToken"),
params = ChooseTokenComponent.Params(
bridge = model.chooseTokenBridge,
),
)
}
}
private val userPortfolioComponent: UserPortfolioComponent by lazy {
userPortfolioComponentFactory.create(
context = child(key = "addFundsUserPortfolio"),
params = UserPortfolioComponent.Params(
uiState = model.userPortfolioStateController.uiState,
callbacks = object : UserPortfolioComponent.Callbacks {
override fun onContinueFromUserPortfolio() = Unit
},
),
)
}
override fun dismiss() = model.onDismiss()
@Composable
override fun Content(modifier: Modifier) {
chooseTokenComponent.Content(modifier)
val isTokenActionsShown by model.isTokenActionsShown.collectAsStateWithLifecycle()
if (isTokenActionsShown) {
// force use redesign theme here according to the task requirements, will be reworked in the next release
TangemThemeRedesign {
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = model::onTokenActionsDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
containerColor = TangemTheme.colors2.surface.level2,
scrollableContent = true,
title = {
TangemModalBottomSheetTitle(
modifier = Modifier.fillMaxWidth(),
title = resourceReference(R.string.common_get_token),
endIconRes = R.drawable.ic_close_24,
onEndClick = model::onTokenActionsDismiss,
)
},
content = { _ ->
Column(
modifier = Modifier.padding(
start = TangemTheme.dimens2.x4,
top = TangemTheme.dimens2.x2,
end = TangemTheme.dimens2.x4,
bottom = TangemTheme.dimens2.x4,
),
) {
tokenActionsComponent.Content(Modifier)
}
},
)
}
override fun BottomSheet() {
val route by model.uiRoute.collectAsStateWithLifecycle()
val canGoBack by model.canGoBack.collectAsStateWithLifecycle()
LaunchedEffect(route) {
if (route != AddFundsModel.UiRoute.UserPortfolio) return@LaunchedEffect
val mode = params.launchMode as? AddFundsComponent.LaunchMode.FilteredByRawId ?: return@LaunchedEffect
model.userPortfolioStateController.updateAndWaitNotNullState(
allAvailableData = model.buildAvailableToAddDataForChooser(),
rawCurrencyId = mode.rawCurrencyId,
)
}
WithOptionalRedesignTheme(isEnabled = isAddFundsStage1Enabled) {
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
onBack = if (canGoBack) model::onBack else ::dismiss,
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
),
type = when (params.launchMode) {
is AddFundsComponent.LaunchMode.TokenActionsOnly -> TangemBottomSheetType.Modal
is AddFundsComponent.LaunchMode.ChooseToken -> TangemBottomSheetType.Default
is AddFundsComponent.LaunchMode.FilteredByRawId ->
if (route is AddFundsModel.UiRoute.TokenActions) {
TangemBottomSheetType.Default
} else {
TangemBottomSheetType.Modal
}
},
containerColor = TangemTheme.colors2.surface.level2,
title = {
AddFundsBottomSheetTitle(
route = route,
canGoBack = canGoBack,
onBackClick = model::onBack,
onCloseClick = ::dismiss,
)
},
content = {
val animatedContentModifier =
if (params.launchMode is AddFundsComponent.LaunchMode.ChooseToken) {
Modifier.fillMaxSize()
} else {
Modifier
}
AnimatedContent(
targetState = route,
modifier = animatedContentModifier,
label = "AddFundsContentAnimation",
) { animatedRoute ->
AddFundsRouteContent(
route = animatedRoute,
shouldFillHeight = !isCompactTokenActions && animatedRoute.uiSpec().shouldFillHeight,
)
}
},
)
}
}
@Composable
private fun AddFundsRouteContent(route: AddFundsModel.UiRoute, shouldFillHeight: Boolean) {
val spec = route.uiSpec()
val horizontalPadding = if (spec.shouldApplyHorizontalPadding) {
Modifier.padding(horizontal = TangemTheme.dimens2.x4)
} else {
Modifier
}
val sizeModifier = if (shouldFillHeight) Modifier.fillMaxSize() else Modifier.fillMaxWidth()
RenderRoute(route, horizontalPadding.then(sizeModifier))
}
@Composable
private fun RenderRoute(route: AddFundsModel.UiRoute, modifier: Modifier = Modifier) {
when (route) {
AddFundsModel.UiRoute.Loading -> Unit
AddFundsModel.UiRoute.ChooseToken -> chooseTokenComponent?.Content(modifier)
AddFundsModel.UiRoute.UserPortfolio -> CompositionLocalProvider(
LocalTangemBottomSheetContentBottomInset provides TangemTheme.dimens2.x4,
) {
userPortfolioComponent.Content(modifier)
}
AddFundsModel.UiRoute.TokenActions -> tokenActionsComponent.Content(modifier)
}
}
@Composable
private fun AddFundsBottomSheetTitle(
route: AddFundsModel.UiRoute,
canGoBack: Boolean,
onBackClick: () -> Unit,
onCloseClick: () -> Unit,
) {
TangemTopBar(
title = route.uiSpec().title,
type = TangemTopBarType.BottomSheet,
startContent = if (canGoBack) {
{ CircleIconButton(iconRes = CoreR.drawable.ic_arrow_back_28, onClick = onBackClick) }
} else {
null
},
endContent = {
CircleIconButton(iconRes = R.drawable.ic_close_24, onClick = onCloseClick)
},
)
}
@Composable
private fun WithOptionalRedesignTheme(isEnabled: Boolean, content: @Composable () -> Unit) {
if (isEnabled) {
TangemThemeRedesign(content = content)
} else {
content()
}
}
@Composable
private fun CircleIconButton(iconRes: Int, onClick: () -> Unit) {
Icon(
imageVector = ImageVector.vectorResource(id = iconRes),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clickableSingle(onClick = onClick)
.padding(TangemTheme.dimens2.x2),
)
}
@AssistedFactory

View file

@ -19,8 +19,6 @@ internal sealed class AddFundsAnalyticsEvent(
class ButtonReceive : AddFundsAnalyticsEvent(event = "Button - Receive")
class ButtonGoToToken : AddFundsAnalyticsEvent(event = "Button - Go to Token")
companion object {
private const val CATEGORY = "Add Funds"
const val SOURCE_MAIN_SCREEN = "Main Screen"

View file

@ -1,5 +1,6 @@
package com.tangem.features.commonfeatures.impl.addfunds.model
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.markets.action.CryptoCurrencyData
@ -8,14 +9,25 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.account.filterCryptoPortfolio
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddData
import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddWallet
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenAnalyticsPayload
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
import com.tangem.features.commonfeatures.api.tokenactions.BottomAction
import com.tangem.features.commonfeatures.impl.addfunds.analytics.AddFundsAnalyticsEvent
import com.tangem.features.commonfeatures.impl.addtoportfolio.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.userportfolio.state.UserPortfolioStateController
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
@ -23,98 +35,257 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
@ModelScoped
@Suppress("LongParameterList")
internal class AddFundsModel @Inject constructor(
paramsContainer: ParamsContainer,
chooseTokenBridgeFactory: ChooseTokenBridge.Factory,
userPortfolioStateControllerFactory: UserPortfolioStateController.Factory,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2,
private val appRouter: AppRouter,
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appRouter: AppRouter,
override val dispatchers: CoroutineDispatcherProvider,
) : Model(), TokenActionsComponent.Callbacks {
private val params = paramsContainer.require<AddFundsComponent.Params>()
val launchMode: AddFundsComponent.LaunchMode = params.launchMode
val chooseTokenBridge: ChooseTokenBridge = chooseTokenBridgeFactory.create(
modelScope = modelScope,
settings = ChooseTokenBridge.Settings.AddFunds,
analyticsPayload = setOf(
ChooseTokenAnalyticsPayload.ScreensSources(SCREEN_SOURCE),
),
)
private val routeStack = MutableStateFlow(listOf<UiRoute>(UiRoute.Loading))
private val selectedToken = MutableStateFlow<ChooseTokenResult?>(null)
val uiRoute: StateFlow<UiRoute> = routeStack
.map { it.last() }
.distinctUntilChanged()
.stateIn(modelScope, SharingStarted.Eagerly, initialValue = UiRoute.Loading)
val isTokenActionsShown: StateFlow<Boolean> = selectedToken
.map { it != null }
val canGoBack: StateFlow<Boolean> = routeStack
.map { it.size > 1 }
.distinctUntilChanged()
.stateIn(modelScope, SharingStarted.Eagerly, initialValue = false)
private val tokenActionsTrigger = MutableStateFlow<TokenActionsRequest?>(null)
private val filteredEntries = MutableStateFlow<List<FilteredEntry>>(emptyList())
val currentBottomAction: MutableStateFlow<BottomAction> =
MutableStateFlow(BottomAction.None)
@OptIn(ExperimentalCoroutinesApi::class)
val tokenActionsData: Flow<CryptoCurrencyData> = selectedToken
val tokenActionsData: Flow<CryptoCurrencyData> = tokenActionsTrigger
.filterNotNull()
.flatMapLatest { result ->
val cryptoPortfolio = result.account as? AccountStatus.CryptoPortfolio
?: return@flatMapLatest emptyFlow()
getCryptoCurrencyActionsUseCase(
accountId = cryptoPortfolio.account.accountId,
currency = result.currency.currency,
).map { actionsState ->
.flatMapLatest { request ->
combine(
getCryptoCurrencyActionsUseCase(
accountId = request.account.account.accountId,
currency = request.status.currency,
),
isAccountsModeEnabledUseCase(),
) { actionsState, isAccountMode ->
CryptoCurrencyData(
userWallet = result.wallet,
status = result.currency,
userWallet = request.userWallet,
status = request.status,
actions = actionsState.states,
isAccountMode = false,
account = cryptoPortfolio,
isAccountMode = isAccountMode,
account = request.account,
)
}
}
val chooseTokenBridge: ChooseTokenBridge by lazy {
chooseTokenBridgeFactory.create(
modelScope = modelScope,
settings = ChooseTokenBridge.Settings.AddFunds,
analyticsPayload = setOf(ChooseTokenAnalyticsPayload.ScreensSources(SCREEN_SOURCE)),
)
}
val userPortfolioStateController: UserPortfolioStateController = userPortfolioStateControllerFactory.create(
modelScope = modelScope,
onTokenSelected = { result ->
openTokenActions(
request = TokenActionsRequest(
userWallet = result.wallet,
account = result.account,
status = result.addedCurrency,
),
bottomAction = BottomAction.None,
)
},
)
init {
chooseTokenBridge.selectWalletTab(params.userWalletId)
analyticsEventHandler.send(
AddFundsAnalyticsEvent.MethodScreenOpened(source = AddFundsAnalyticsEvent.SOURCE_MAIN_SCREEN),
)
observeBridge()
when (val mode = launchMode) {
is AddFundsComponent.LaunchMode.ChooseToken -> initChooseToken(mode)
is AddFundsComponent.LaunchMode.TokenActionsOnly -> initTokenActionsOnly(mode)
is AddFundsComponent.LaunchMode.FilteredByRawId -> initFilteredByRawId(mode)
}
}
override fun onBottomActionClick() {
val result = selectedToken.value ?: return
selectedToken.value = null
analyticsEventHandler.send(AddFundsAnalyticsEvent.ButtonGoToToken())
appRouter.replaceCurrent(
AppRoute.CurrencyDetails(
userWalletId = result.wallet.walletId,
currency = result.currency.currency,
),
)
fun onBack() {
routeStack.update { stack -> if (stack.size > 1) stack.dropLast(1) else stack }
}
override fun onQuickActionClick(action: TokenActionsBSContentUM.Action) {
fun onDismiss() = params.onDismiss()
override fun onBottomActionClick(bottomAction: BottomAction) {
val request = tokenActionsTrigger.value
if (bottomAction == BottomAction.GoToToken && request != null) {
appRouter.push(
AppRoute.CurrencyDetails(
userWalletId = request.userWallet.walletId,
currency = request.status.currency,
),
)
}
params.onDismiss()
}
override fun onQuickActionClick(action: TokenActionsBSContentUM.Action, shouldDismiss: Boolean) {
val event = when (action) {
TokenActionsBSContentUM.Action.Buy -> AddFundsAnalyticsEvent.ButtonBuy()
TokenActionsBSContentUM.Action.Exchange -> AddFundsAnalyticsEvent.ButtonSwap()
TokenActionsBSContentUM.Action.Receive -> AddFundsAnalyticsEvent.ButtonReceive()
else -> return
else -> null
}
event?.let { analyticsEventHandler.send(it) }
if (shouldDismiss) {
params.onDismiss()
}
analyticsEventHandler.send(event)
}
fun onTokenActionsDismiss() {
selectedToken.value = null
fun buildAvailableToAddDataForChooser(): AvailableToAddData {
val byWallet = filteredEntries.value.groupBy { it.userWallet.walletId }
return AvailableToAddData(
availableToAddWallets = byWallet.mapValues { (_, entries) ->
AvailableToAddWallet(
userWallet = entries.first().userWallet,
accounts = entries.map { it.account }.distinct(),
availableNetworks = emptySet(),
availableToAddAccounts = emptyMap(),
)
},
)
}
private fun observeBridge() {
private fun initChooseToken(mode: AddFundsComponent.LaunchMode.ChooseToken) {
chooseTokenBridge.selectWalletTab(mode.userWalletId)
analyticsEventHandler.send(
AddFundsAnalyticsEvent.MethodScreenOpened(source = AddFundsAnalyticsEvent.SOURCE_MAIN_SCREEN),
)
replaceRoot(UiRoute.ChooseToken)
modelScope.launch {
chooseTokenBridge.onCurrencyChosen.receiveAsFlow().collect { result ->
selectedToken.value = result
}
chooseTokenBridge.onCurrencyChosen.receiveAsFlow().collect(::openTokenActionsFromBridge)
}
modelScope.launch {
chooseTokenBridge.onClose.receiveAsFlow().collect {
appRouter.pop()
chooseTokenBridge.onClose.receiveAsFlow().collect { params.onDismiss() }
}
}
private fun initTokenActionsOnly(mode: AddFundsComponent.LaunchMode.TokenActionsOnly) {
modelScope.launch {
val wallet = getUserWalletUseCase.invokeFlow(mode.userWalletId)
.mapNotNull { it.getOrNull() }
.first()
val match = multiAccountStatusListSupplier()
.first()
.firstOrNull { it.userWalletId == mode.userWalletId }
?.accountStatuses
?.filterCryptoPortfolio()
?.firstNotNullOfOrNull { accountStatus ->
accountStatus.tokenList.flattenCurrencies()
.firstOrNull { it.currency.id == mode.currency.id }
?.let { accountStatus to it }
}
?: run {
params.onDismiss()
return@launch
}
tokenActionsTrigger.value = TokenActionsRequest(wallet, match.first, match.second)
replaceRoot(UiRoute.TokenActions)
}
}
private fun initFilteredByRawId(mode: AddFundsComponent.LaunchMode.FilteredByRawId) {
modelScope.launch {
val entries = collectFilteredEntries(mode.rawCurrencyId)
when (entries.size) {
0 -> params.onDismiss()
1 -> {
val entry = entries.first()
tokenActionsTrigger.value = TokenActionsRequest(entry.userWallet, entry.account, entry.status)
replaceRoot(UiRoute.TokenActions)
}
else -> {
filteredEntries.value = entries
replaceRoot(UiRoute.UserPortfolio)
}
}
}
}
private suspend fun collectFilteredEntries(rawCurrencyId: CryptoCurrency.RawID): List<FilteredEntry> {
val accountLists = multiAccountStatusListSupplier().first()
return accountLists.flatMap { accountStatusList ->
val wallet = getUserWalletUseCase.invokeFlow(accountStatusList.userWalletId)
.mapNotNull { it.getOrNull() }
.firstOrNull()
?: return@flatMap emptyList()
accountStatusList.accountStatuses.filterCryptoPortfolio().flatMap { accountStatus ->
accountStatus.tokenList.flattenCurrencies()
.filter { status ->
val id = status.currency.id.rawCurrencyId ?: return@filter false
getTokenIdIfL2Network(id.value) == rawCurrencyId.value
}
.map { status -> FilteredEntry(wallet, accountStatus, status) }
}
}
}
private fun openTokenActionsFromBridge(result: ChooseTokenResult) {
val account = result.account as? AccountStatus.CryptoPortfolio ?: return
openTokenActions(
request = TokenActionsRequest(result.wallet, account, result.currency),
bottomAction = if (result.wasJustAdded) {
BottomAction.GoToToken
} else {
BottomAction.None
},
)
}
private fun openTokenActions(request: TokenActionsRequest, bottomAction: BottomAction) {
tokenActionsTrigger.value = request
currentBottomAction.value = bottomAction
pushRoute(UiRoute.TokenActions)
}
private fun replaceRoot(route: UiRoute) {
routeStack.value = listOf(route)
}
private fun pushRoute(route: UiRoute) {
routeStack.update { it + route }
}
sealed interface UiRoute {
data object Loading : UiRoute
data object ChooseToken : UiRoute
data object UserPortfolio : UiRoute
data object TokenActions : UiRoute
}
private data class TokenActionsRequest(
val userWallet: UserWallet,
val account: AccountStatus.CryptoPortfolio,
val status: CryptoCurrencyStatus,
)
private data class FilteredEntry(
val userWallet: UserWallet,
val account: AccountStatus.CryptoPortfolio,
val status: CryptoCurrencyStatus,
)
private companion object {
const val SCREEN_SOURCE = "AddFunds"
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.commonfeatures.impl.addfunds.model
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.commonfeatures.impl.R
import com.tangem.core.ui.R as CoreR
internal data class AddFundsRouteUiSpec(
val title: TextReference,
val shouldApplyHorizontalPadding: Boolean,
val shouldFillHeight: Boolean,
)
internal fun AddFundsModel.UiRoute.uiSpec(): AddFundsRouteUiSpec = when (this) {
AddFundsModel.UiRoute.Loading -> AddFundsRouteUiSpec(
title = resourceReference(R.string.common_add_funds),
shouldApplyHorizontalPadding = false,
shouldFillHeight = false,
)
AddFundsModel.UiRoute.ChooseToken -> AddFundsRouteUiSpec(
title = resourceReference(R.string.common_add_funds),
shouldApplyHorizontalPadding = false,
shouldFillHeight = true,
)
AddFundsModel.UiRoute.UserPortfolio -> AddFundsRouteUiSpec(
title = resourceReference(R.string.common_add_funds),
shouldApplyHorizontalPadding = false,
shouldFillHeight = false,
)
AddFundsModel.UiRoute.TokenActions -> AddFundsRouteUiSpec(
title = resourceReference(CoreR.string.common_get_token),
shouldApplyHorizontalPadding = true,
shouldFillHeight = true,
)
}

View file

@ -23,7 +23,7 @@ import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioFooterKind
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.uiSpec
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM
import com.tangem.features.commonfeatures.impl.userportfolio.model.UserPortfolioUM
import dev.chrisbanes.haze.rememberHazeState
@Composable

View file

@ -6,7 +6,7 @@ import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM
import com.tangem.features.commonfeatures.impl.userportfolio.model.UserPortfolioUM
@Composable
internal fun AddToPortfolioBottomSheetSwitch(

View file

@ -22,7 +22,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.uiSpec
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM
import com.tangem.features.commonfeatures.impl.userportfolio.model.UserPortfolioUM
@Composable
internal fun AddToPortfolioBottomSheetV2(
@ -39,6 +39,11 @@ internal fun AddToPortfolioBottomSheetV2(
contentStack.value = stack
}
val type = if (stack.active.configuration is AddToPortfolioRoutes.TokenActions) {
TangemBottomSheetType.Default
} else {
TangemBottomSheetType.Modal
}
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
onBack = onBack,
config = TangemBottomSheetConfig(
@ -46,7 +51,7 @@ internal fun AddToPortfolioBottomSheetV2(
onDismissRequest = onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
type = TangemBottomSheetType.Modal,
type = type,
containerColor = TangemTheme.colors2.surface.level2,
title = {
AddToPortfolioBottomSheetTitle(
@ -86,7 +91,9 @@ private fun AddToPortfolioRouteContent(animatedStack: ChildStack<AddToPortfolioR
Spacer(modifier = Modifier.height(scrollBottomReserve))
}
} else {
animatedStack.active.instance.Content(modifier = baseModifier)
val isFullScreenRoute = animatedStack.active.configuration is AddToPortfolioRoutes.TokenActions
val sizeModifier = if (isFullScreenRoute) Modifier.fillMaxSize() else Modifier
animatedStack.active.instance.Content(modifier = baseModifier.then(sizeModifier))
}
}

View file

@ -15,11 +15,14 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioModel
import com.tangem.features.commonfeatures.api.tokenactions.BottomAction
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.UserPortfolioComponent
import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.userportfolio.UserPortfolioComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.flowOf
@Suppress("LongParameterList")
internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
@ -60,6 +63,7 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
params = TokenActionsComponent.Params(
callbacks = model,
data = model.tokenActionsData,
bottomAction = flowOf(BottomAction.GoToToken),
),
)
}

View file

@ -92,13 +92,5 @@ internal class PortfolioAnalyticsEvent(
if (source != null) put("Source", source)
},
)
fun getTokenLater() = PortfolioAnalyticsEvent(
event = "Popup Get token - Button Later",
category = category,
params = buildMap {
if (source != null) put("Source", source)
},
)
}
}

View file

@ -4,8 +4,8 @@ import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioCompo
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.commonfeatures.impl.addtoportfolio.DefaultAddToPortfolioComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.DefaultAddToPortfolioManager
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.DefaultUserPortfolioComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.UserPortfolioComponent
import com.tangem.features.commonfeatures.impl.userportfolio.DefaultUserPortfolioComponent
import com.tangem.features.commonfeatures.impl.userportfolio.UserPortfolioComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn

View file

@ -5,8 +5,8 @@ import com.tangem.core.decompose.model.Model
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioModel
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddTokenModel
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.ChooseNetworkModel
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.TokenActionsModel
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioModel
import com.tangem.features.commonfeatures.impl.tokenactions.model.TokenActionsModel
import com.tangem.features.commonfeatures.impl.userportfolio.model.UserPortfolioModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn

View file

@ -29,18 +29,20 @@ import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.commonfeatures.api.addtoportfolio.*
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorController
import com.tangem.features.commonfeatures.api.tokenactions.BottomAction
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.addtoportfolio.AddTokenComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.ChooseNetworkComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.analytics.PortfolioAnalyticsEvent
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.UserPortfolioComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.state.UserPortfolioStateController
import com.tangem.features.commonfeatures.impl.userportfolio.UserPortfolioComponent
import com.tangem.features.commonfeatures.impl.userportfolio.state.UserPortfolioStateController
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -107,10 +109,6 @@ internal class AddToPortfolioModel @Inject constructor(
startRedesignAddToPortfolioFlow()
}
override fun onQuickActionClick(action: TokenActionsBSContentUM.Action) {
analyticsEventHandler.send(eventBuilder.getTokenActionClick(actionUM = action))
}
private fun <T> replayMutableSharedFlow() = MutableSharedFlow<T>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
@ -155,7 +153,7 @@ internal class AddToPortfolioModel @Inject constructor(
}
}
@Suppress("LongMethod")
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun startRedesignAddToPortfolioFlow() {
channelFlow<Unit> {
fun finishSuccessFlow(result: AddToPortfolioManager.Result) {
@ -312,9 +310,15 @@ internal class AddToPortfolioModel @Inject constructor(
.onEmpty { finishSuccessFlow(result) }
.launchIn(this)
callbackDelegate.onChooseTokenBottomActionClick.receiveAsFlow().first()
analyticsEventHandler.send(eventBuilder.getTokenLater())
finishSuccessFlow(result)
when (val meta = terminalTokenActionsFlow().first()) {
is AddToPortfolioManager.FinishMeta.OnBottomAction -> {
finishSuccessFlow(result.copy(meta = meta))
}
AddToPortfolioManager.FinishMeta.OnQuickAction -> {
finishSuccessFlow(result.copy(meta = meta))
}
AddToPortfolioManager.FinishMeta.None -> Unit
}
}
.catch { throwable ->
TangemLogger.e("Error", throwable)
@ -323,6 +327,21 @@ internal class AddToPortfolioModel @Inject constructor(
.launchIn(modelScope)
}
private fun terminalTokenActionsFlow() = channelFlow {
callbackDelegate.onChooseTokenBottomActionClick.receiveAsFlow()
.onEach { bottomAction ->
channel.send(AddToPortfolioManager.FinishMeta.OnBottomAction(bottomAction))
}
.launchIn(this)
callbackDelegate.onQuickActionClick.receiveAsFlow()
.onEach { (action, shouldDismiss) ->
analyticsEventHandler.send(eventBuilder.getTokenActionClick(actionUM = action))
if (shouldDismiss) channel.send(AddToPortfolioManager.FinishMeta.OnQuickAction)
}
.launchIn(this)
awaitClose()
}
private suspend fun getInitialSelection(
initialData: AvailableToAddData,
): AddToPortfolioInitialSelectionResolver.InitialSelection? {
@ -529,7 +548,8 @@ internal class AddToPortfolioCallbackDelegate @Inject constructor() :
UserPortfolioComponent.Callbacks {
val onNetworkSelected = Channel<TokenMarketInfo.Network>()
val onChooseTokenBottomActionClick = Channel<Unit>()
val onChooseTokenBottomActionClick = Channel<BottomAction>()
val onQuickActionClick = Channel<Pair<TokenActionsBSContentUM.Action, Boolean>>()
val onChangeNetworkClick = Channel<Unit>()
val onChangePortfolioClick = Channel<Unit>()
val onTokenAdded = Channel<CryptoCurrencyStatus>()
@ -539,8 +559,12 @@ internal class AddToPortfolioCallbackDelegate @Inject constructor() :
onNetworkSelected.trySend(network)
}
override fun onBottomActionClick() {
onChooseTokenBottomActionClick.trySend(Unit)
override fun onBottomActionClick(bottomAction: BottomAction) {
onChooseTokenBottomActionClick.trySend(bottomAction)
}
override fun onQuickActionClick(action: TokenActionsBSContentUM.Action, shouldDismiss: Boolean) {
onQuickActionClick.trySend(action to shouldDismiss)
}
override fun onChangeNetworkClick() {

View file

@ -44,7 +44,7 @@ internal fun AddToPortfolioRoutes.uiSpec(): AddToPortfolioRouteUiSpec = when (th
)
AddToPortfolioRoutes.TokenActions -> AddToPortfolioRouteUiSpec(
title = resourceReference(R.string.common_get_token),
isScrollable = true,
isScrollable = false,
shouldApplyHorizontalPadding = true,
footer = AddToPortfolioFooterKind.None,
)

View file

@ -41,7 +41,7 @@ internal class DefaultChooseTokenComponent @AssistedInject constructor(
override fun Content(modifier: Modifier) {
val bottomSheet by bottomSheetSlot.subscribeAsState()
val state by model.state.collectAsStateWithLifecycle()
ChooseTokenScreen(state = state)
ChooseTokenScreen(state = state, modifier = modifier)
bottomSheet.child?.instance?.BottomSheet()
}

View file

@ -6,6 +6,8 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.commonfeatures.api.R
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenAnalyticsPayload
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery
@ -14,10 +16,9 @@ import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
import com.tangem.features.commonfeatures.impl.choosetoken.converter.SearchBarToggleTransformer
import com.tangem.features.commonfeatures.impl.choosetoken.converter.SearchBarUpdateQueryTransformer
import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketState
import com.tangem.features.commonfeatures.impl.choosetoken.ui.ChooseTokenFullUM
import com.tangem.features.commonfeatures.impl.choosetoken.ui.ChooseTokenInitialUM
import com.tangem.features.commonfeatures.api.R
import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ -78,18 +79,10 @@ internal class ChooseTokenModel @Inject constructor(
.onEach { marketBlockDelegate.addToPortfolioSlot.dismiss() }
.launchIn(modelScope)
addToPortfolioManager.onSuccessAdded.receiveAsFlow()
.onEach { addedResult ->
val isSearched = ChooseTokenAnalyticsPayload.IsSearched(isSearchingState)
val isMarketToken = ChooseTokenAnalyticsPayload.IsMarketTokenSelected(true)
val chooseTokenResult = ChooseTokenResult(
currency = addedResult.addedCurrency,
account = addedResult.account,
wallet = addedResult.wallet,
analyticsPayload = setOf(isSearched, isMarketToken),
)
bridge.onCurrencyChosen(chooseTokenResult)
marketBlockDelegate.addToPortfolioSlot.dismiss()
}
.onEach { notifyCurrencyChosen(it, isMarketTokenSelected = true) }
.launchIn(modelScope)
addToPortfolioManager.onAddedTokenClick.receiveAsFlow()
.onEach { notifyCurrencyChosen(it, isMarketTokenSelected = false) }
.launchIn(modelScope)
}
@ -97,6 +90,20 @@ internal class ChooseTokenModel @Inject constructor(
bridge.onClose()
}
private fun notifyCurrencyChosen(addedResult: AddToPortfolioManager.Result, isMarketTokenSelected: Boolean) {
val chooseTokenResult = ChooseTokenResult(
currency = addedResult.addedCurrency,
account = addedResult.account,
wallet = addedResult.wallet,
analyticsPayload = setOf(
ChooseTokenAnalyticsPayload.IsSearched(isSearchingState),
ChooseTokenAnalyticsPayload.IsMarketTokenSelected(isMarketTokenSelected),
),
)
bridge.onCurrencyChosen(chooseTokenResult)
marketBlockDelegate.addToPortfolioSlot.dismiss()
}
private fun getInitialSearchBar(): SearchBarUM = SearchBarUM(
placeholderText = resourceReference(R.string.common_search),
query = "",
@ -112,6 +119,7 @@ internal class ChooseTokenModel @Inject constructor(
private fun getInitState() = ChooseTokenInitialUM(
screenTitle = bridge.settings.title,
isAppBarShown = bridge.settings.isAppBarShown,
onCloseClick = ::onBackClicked,
searchBar = getInitialSearchBar(),
)

View file

@ -82,12 +82,14 @@ private val ChooseTokenFullUM.isEmptyState: Boolean
internal fun ChooseTokenScreen(state: ChooseTokenFullUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(color = TangemTheme.colors.background.secondary)
.background(color = TangemTheme.colors2.surface.level2)
.fillMaxSize()
.imePadding(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AppBar(title = state.initialUM.screenTitle, onBackClick = state.initialUM.onCloseClick, Modifier)
if (state.initialUM.isAppBarShown) {
AppBar(title = state.initialUM.screenTitle, onBackClick = state.initialUM.onCloseClick, Modifier)
}
Content(
state = state,
@ -465,6 +467,7 @@ private val wallets
private val initialUM = ChooseTokenInitialUM(
screenTitle = stringReference("Choose token"),
isAppBarShown = true,
onCloseClick = {},
searchBar = searchBar,
)

View file

@ -13,6 +13,7 @@ internal data class ChooseTokenFullUM(
internal data class ChooseTokenInitialUM(
val screenTitle: TextReference,
val isAppBarShown: Boolean,
val onCloseClick: () -> Unit,
val searchBar: SearchBarUM,
)

View file

@ -1,4 +1,4 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio
package com.tangem.features.commonfeatures.impl.tokenactions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -18,14 +18,16 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.TokenActionsModel
import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.TokenActionsContent
import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.TokenActionsContentV2
import com.tangem.features.commonfeatures.api.tokenactions.BottomAction
import com.tangem.features.commonfeatures.impl.tokenactions.model.TokenActionsModel
import com.tangem.features.commonfeatures.impl.tokenactions.ui.TokenActionsContent
import com.tangem.features.commonfeatures.impl.tokenactions.ui.TokenActionsContentV2
import com.tangem.features.tokenreceive.TokenReceiveComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
internal class TokenActionsComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@ -74,15 +76,14 @@ internal class TokenActionsComponent @AssistedInject constructor(
data class Params(
val data: Flow<CryptoCurrencyData>,
val callbacks: Callbacks,
val bottomAction: BottomAction = BottomAction.Later,
val bottomAction: Flow<BottomAction> = flowOf(BottomAction.None),
val isRedesignForced: Boolean = false,
val isCompact: Boolean = false,
)
enum class BottomAction { Later, GoToToken }
interface Callbacks {
fun onBottomActionClick()
fun onQuickActionClick(action: TokenActionsBSContentUM.Action) {}
fun onBottomActionClick(bottomAction: BottomAction)
fun onQuickActionClick(action: TokenActionsBSContentUM.Action, shouldDismiss: Boolean) {}
}
@AssistedFactory

View file

@ -1,4 +1,4 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio.model
package com.tangem.features.commonfeatures.impl.tokenactions.model
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
@ -12,8 +12,8 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory
import com.tangem.features.commonfeatures.impl.addtoportfolio.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.TokenActionsUM
import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.tokenactions.ui.state.TokenActionsUM
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -45,7 +45,9 @@ internal class TokenActionsModel @Inject constructor(
private val tokenActionsHandler: TokenActionsHandler =
tokenActionsIntentsFactory.create(
currentAppCurrency = Provider { currentAppCurrency.value },
onHandleQuickAction = { handledAction -> handledQuickAction(handledAction) },
onHandleQuickAction = { handledAction, shouldDismiss ->
handledQuickAction(handledAction, shouldDismiss)
},
)
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = SlotNavigation()
@ -55,15 +57,17 @@ internal class TokenActionsModel @Inject constructor(
combine(
params.data,
getBalanceHidingSettingsUseCase.isBalanceHidden(),
) { cryptoCurrencyData, isBalanceHidden ->
cryptoCurrencyData to isBalanceHidden
params.bottomAction,
) { cryptoCurrencyData, isBalanceHidden, bottomAction ->
Triple(cryptoCurrencyData, isBalanceHidden, bottomAction)
}
.mapLatest { (cryptoCurrencyData, isBalanceHidden) ->
.mapLatest { (cryptoCurrencyData, isBalanceHidden, bottomAction) ->
uiBuilder.build(
cryptoCurrencyData = cryptoCurrencyData,
tokenActionsHandler = tokenActionsHandler,
appCurrency = currentAppCurrency.value,
isBalanceHidden = isBalanceHidden,
bottomAction = bottomAction,
)
}
.flowOn(dispatchers.default)
@ -73,16 +77,18 @@ internal class TokenActionsModel @Inject constructor(
initialValue = null,
)
private fun handledQuickAction(handledAction: TokenActionsHandler.HandledQuickAction) = modelScope.launch {
params.callbacks.onQuickActionClick(handledAction.action)
val isReceive = handledAction.action == TokenActionsBSContentUM.Action.Receive
if (!isReceive) return@launch
val tokenConfig = withContext(dispatchers.default) {
receiveAddressesFactory.create(
status = handledAction.cryptoCurrencyData.status,
userWalletId = handledAction.cryptoCurrencyData.userWallet.walletId,
)
} ?: return@launch
bottomSheetNavigation.activate(tokenConfig)
}
private fun handledQuickAction(handledAction: TokenActionsHandler.HandledQuickAction, shouldDismiss: Boolean) =
modelScope.launch {
val isReceive = handledAction.action == TokenActionsBSContentUM.Action.Receive
if (isReceive) {
val tokenConfig = withContext(dispatchers.default) {
receiveAddressesFactory.create(
status = handledAction.cryptoCurrencyData.status,
userWalletId = handledAction.cryptoCurrencyData.userWallet.walletId,
)
}
if (tokenConfig != null) bottomSheetNavigation.activate(tokenConfig)
}
params.callbacks.onQuickActionClick(handledAction.action, shouldDismiss)
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio.model
package com.tangem.features.commonfeatures.impl.tokenactions.model
import androidx.compose.ui.text.SpanStyle
import com.tangem.common.getTotalCryptoAmount
@ -11,6 +11,7 @@ import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToI
import com.tangem.common.ui.markets.action.CryptoCurrencyData
import com.tangem.common.ui.markets.action.QuickActionsConverter.quickActions
import com.tangem.common.ui.markets.action.TokenActionsHandler
import com.tangem.features.commonfeatures.api.tokenactions.BottomAction
import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.ParamsContainer
@ -30,9 +31,9 @@ import com.tangem.features.commonfeatures.impl.R
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.wallets.usecase.GetWalletIconUseCase
import com.tangem.features.commonfeatures.impl.addtoportfolio.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.PortfolioBadgeUM
import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.TokenActionsUM
import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.tokenactions.ui.state.PortfolioBadgeUM
import com.tangem.features.commonfeatures.impl.tokenactions.ui.state.TokenActionsUM
import java.math.BigDecimal
import javax.inject.Inject
@ -50,6 +51,7 @@ internal class TokenActionsUiBuilder @Inject constructor(
tokenActionsHandler: TokenActionsHandler,
appCurrency: AppCurrency,
isBalanceHidden: Boolean,
bottomAction: BottomAction,
): TokenActionsUM {
return if (designFeatureToggles.isRedesignEnabled || params.isRedesignForced) {
buildV2(
@ -57,11 +59,13 @@ internal class TokenActionsUiBuilder @Inject constructor(
tokenActionsHandler = tokenActionsHandler,
appCurrency = appCurrency,
isBalanceHidden = isBalanceHidden,
bottomAction = bottomAction,
)
} else {
buildV1(
cryptoCurrencyData = cryptoCurrencyData,
tokenActionsHandler = tokenActionsHandler,
bottomAction = bottomAction,
)
}
}
@ -69,6 +73,7 @@ internal class TokenActionsUiBuilder @Inject constructor(
private fun buildV1(
cryptoCurrencyData: CryptoCurrencyData,
tokenActionsHandler: TokenActionsHandler,
bottomAction: BottomAction,
): TokenActionsUM {
val status = cryptoCurrencyData.status
val tokenUM = TokenItemState.Content(
@ -88,9 +93,9 @@ internal class TokenActionsUiBuilder @Inject constructor(
tokenActionsHandler = tokenActionsHandler,
isRedesignEnabled = false,
),
bottomActionText = bottomActionText(params.bottomAction),
bottomActionText = bottomActionText(bottomAction),
onBottomActionClick = {
params.callbacks.onBottomActionClick()
params.callbacks.onBottomActionClick(bottomAction)
},
)
}
@ -100,6 +105,7 @@ internal class TokenActionsUiBuilder @Inject constructor(
tokenActionsHandler: TokenActionsHandler,
appCurrency: AppCurrency,
isBalanceHidden: Boolean,
bottomAction: BottomAction,
): TokenActionsUM {
val status = cryptoCurrencyData.status
val tokenUM = TokenItemState.Content(
@ -119,19 +125,20 @@ internal class TokenActionsUiBuilder @Inject constructor(
tokenActionsHandler = tokenActionsHandler,
isRedesignEnabled = true,
),
bottomActionText = bottomActionText(params.bottomAction),
bottomActionText = bottomActionText(bottomAction),
onBottomActionClick = {
params.callbacks.onBottomActionClick()
params.callbacks.onBottomActionClick(bottomAction)
},
isBalancesHidden = isBalanceHidden,
portfolioBadge = createPortfolioBadge(cryptoCurrencyData = cryptoCurrencyData),
isCompact = params.isCompact,
)
}
private fun bottomActionText(action: TokenActionsComponent.BottomAction): TextReference {
private fun bottomActionText(action: BottomAction): TextReference? {
return when (action) {
TokenActionsComponent.BottomAction.Later -> resourceReference(R.string.common_later)
TokenActionsComponent.BottomAction.GoToToken -> resourceReference(R.string.common_go_to_token)
BottomAction.GoToToken -> resourceReference(R.string.common_go_to_token)
BottomAction.None -> null
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio.ui
package com.tangem.features.commonfeatures.impl.tokenactions.ui
import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
@ -39,7 +39,7 @@ 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.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.TokenActionsUM
import com.tangem.features.commonfeatures.impl.tokenactions.ui.state.TokenActionsUM
import kotlinx.collections.immutable.persistentListOf
import java.util.UUID
@ -73,13 +73,15 @@ internal fun TokenActionsContent(state: TokenActionsUM, modifier: Modifier = Mod
}
}
SpacerH16()
if (state.bottomActionText != null) {
SpacerH16()
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = state.bottomActionText.resolveReference(),
onClick = state.onBottomActionClick,
)
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = state.bottomActionText.resolveReference(),
onClick = state.onBottomActionClick,
)
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio.ui
package com.tangem.features.commonfeatures.impl.tokenactions.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
@ -43,8 +43,8 @@ import com.tangem.core.ui.format.bigdecimal.formatStyled
import com.tangem.core.ui.format.bigdecimal.price
import com.tangem.core.ui.res.*
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.PortfolioBadgeUM
import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.TokenActionsUM
import com.tangem.features.commonfeatures.impl.tokenactions.ui.state.PortfolioBadgeUM
import com.tangem.features.commonfeatures.impl.tokenactions.ui.state.TokenActionsUM
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
@ -52,53 +52,84 @@ import java.util.UUID
@Composable
internal fun TokenActionsContentV2(state: TokenActionsUM, modifier: Modifier = Modifier) {
if (state.isCompact) {
CompactLayout(state, modifier)
} else {
FullLayout(state, modifier)
}
}
@Composable
private fun CompactLayout(state: TokenActionsUM, modifier: Modifier = Modifier) {
Column(modifier = modifier.fillMaxWidth()) {
QuickActionsList(state)
SpacerH(TangemTheme.dimens2.x4)
}
}
@Composable
private fun FullLayout(state: TokenActionsUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxWidth(),
modifier = modifier
.fillMaxSize()
.navigationBarsPadding(),
) {
TokenHeader(
addedToken = state.token,
portfolioBadge = state.portfolioBadge,
isBalanceHidden = state.isBalancesHidden,
)
SpacerH(TangemTheme.dimens2.x2)
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
state.quickActions.actions.fastForEach { actionUM ->
key(actionUM.title) {
val transitionState = remember {
MutableTransitionState(initialState = false).apply { targetState = true }
}
AnimatedVisibility(
visibleState = transitionState,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
TokenActionRow(
iconRes = actionUM.icon,
title = actionUM.title,
description = actionUM.description,
onClick = { state.quickActions.onQuickActionClick(actionUM) },
onLongClick = { state.quickActions.onQuickActionLongClick(actionUM) }
.takeIf { actionUM.isLongClickAvailable },
)
}
}
TokenHeader(
addedToken = state.token,
portfolioBadge = state.portfolioBadge,
isBalanceHidden = state.isBalancesHidden,
)
}
QuickActionsList(state)
val bottomText = state.bottomActionText
if (bottomText != null) {
SpacerH(TangemTheme.dimens2.x6)
CompositionLocalProvider(LocalHazeState provides rememberHazeState()) {
SecondaryTangemButton(
modifier = Modifier.fillMaxWidth(),
onClick = state.onBottomActionClick,
text = bottomText,
size = TangemButtonSize.X12,
shape = TangemButtonShape.Rounded,
)
}
}
SpacerH(TangemTheme.dimens2.x4)
}
}
SpacerH(TangemTheme.dimens2.x6)
CompositionLocalProvider(LocalHazeState provides rememberHazeState()) {
SecondaryTangemButton(
modifier = Modifier.fillMaxWidth(),
onClick = state.onBottomActionClick,
text = state.bottomActionText,
size = TangemButtonSize.X12,
shape = TangemButtonShape.Rounded,
)
@Composable
private fun QuickActionsList(state: TokenActionsUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
state.quickActions.actions.fastForEach { actionUM ->
key(actionUM.title) {
val transitionState = remember {
MutableTransitionState(initialState = false).apply { targetState = true }
}
AnimatedVisibility(
visibleState = transitionState,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
TokenActionRow(
iconRes = actionUM.icon,
title = actionUM.title,
description = actionUM.description,
onClick = { state.quickActions.onQuickActionClick(actionUM) },
onLongClick = { state.quickActions.onQuickActionLongClick(actionUM) }
.takeIf { actionUM.isLongClickAvailable },
)
}
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state
package com.tangem.features.commonfeatures.impl.tokenactions.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.markets.action.QuickActions
@ -10,10 +10,11 @@ import com.tangem.core.ui.extensions.TextReference
internal data class TokenActionsUM(
val token: TokenItemState,
val quickActions: QuickActions,
val bottomActionText: TextReference,
val bottomActionText: TextReference?,
val onBottomActionClick: () -> Unit,
val isBalancesHidden: Boolean = false,
val portfolioBadge: PortfolioBadgeUM = PortfolioBadgeUM.None,
val isCompact: Boolean = false,
)
@Immutable

View file

@ -1,4 +1,4 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio
package com.tangem.features.commonfeatures.impl.userportfolio
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -7,7 +7,7 @@ import com.tangem.common.ui.markets.tokenselector.TokenSelectorEmbeddedContent
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.LocalTangemBottomSheetContentBottomInset
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioModel
import com.tangem.features.commonfeatures.impl.userportfolio.model.UserPortfolioModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

View file

@ -1,8 +1,8 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio
package com.tangem.features.commonfeatures.impl.userportfolio
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM
import com.tangem.features.commonfeatures.impl.userportfolio.model.UserPortfolioUM
import kotlinx.coroutines.flow.StateFlow
internal interface UserPortfolioComponent : ComposableContentComponent {

View file

@ -1,9 +1,9 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model
package com.tangem.features.commonfeatures.impl.userportfolio.model
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.UserPortfolioComponent
import com.tangem.features.commonfeatures.impl.userportfolio.UserPortfolioComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject

View file

@ -1,4 +1,4 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model
package com.tangem.features.commonfeatures.impl.userportfolio.model
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.markets.tokenselector.TokenSelectorContentUM

View file

@ -1,4 +1,4 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.state
package com.tangem.features.commonfeatures.impl.userportfolio.state
import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -7,8 +7,8 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.usecase.GetWalletIconUseCase
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddData
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.transformer.UserPortfolioSectionsTransformer
import com.tangem.features.commonfeatures.impl.userportfolio.model.UserPortfolioUM
import com.tangem.features.commonfeatures.impl.userportfolio.transformer.UserPortfolioSectionsTransformer
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

View file

@ -1,4 +1,4 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.transformer
package com.tangem.features.commonfeatures.impl.userportfolio.transformer
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
import com.tangem.common.ui.account.toUM
@ -21,7 +21,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddData
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM
import com.tangem.features.commonfeatures.impl.userportfolio.model.UserPortfolioUM
import com.tangem.utils.StringsSigns
import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.toImmutableList