Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-19 17:34:42 +02:00
commit bf0a8efd7e
97 changed files with 1537 additions and 507 deletions

View file

@ -55,6 +55,13 @@ interface ChooseTokenBridge : ChooseTokenBridgeInternal {
isAppBarShown = false,
isShowSingleCurrencyWallets = true,
)
val Transfer = Settings(
title = resourceReference(R.string.common_transfer),
isShowMarketBlock = false,
isShowPaymentAccount = false,
isAppBarShown = false,
isShowSingleCurrencyWallets = true,
)
}
}

View file

@ -1,17 +1,20 @@
package com.tangem.features.commonfeatures.api.addfunds
package com.tangem.features.commonfeatures.api.managefunds
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
interface AddFundsComponent : ComposableBottomSheetComponent {
interface ManageFundsComponent : ComposableBottomSheetComponent {
data class Params(
val launchMode: LaunchMode,
val onDismiss: () -> Unit,
val flowType: FlowType = FlowType.AddFunds,
)
enum class FlowType { AddFunds, Transfer }
sealed interface LaunchMode {
data class ChooseToken(val userWalletId: UserWalletId) : LaunchMode
@ -25,5 +28,5 @@ interface AddFundsComponent : ComposableBottomSheetComponent {
) : LaunchMode
}
interface Factory : ComponentFactory<Params, AddFundsComponent>
interface Factory : ComponentFactory<Params, ManageFundsComponent>
}

View file

@ -1,16 +0,0 @@
package com.tangem.features.commonfeatures.impl.addfunds.di
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
import com.tangem.features.commonfeatures.impl.addfunds.DefaultAddFundsComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal interface AddFundsComponentModule {
@Binds
fun bindAddFundsComponentFactory(factory: DefaultAddFundsComponent.Factory): AddFundsComponent.Factory
}

View file

@ -1,35 +0,0 @@
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

@ -1,4 +1,4 @@
package com.tangem.features.commonfeatures.impl.addfunds
package com.tangem.features.commonfeatures.impl.managefunds
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.fillMaxSize
@ -22,51 +22,56 @@ import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemeRedesign
import com.tangem.core.ui.test.BaseBottomSheetTestTags
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
import com.tangem.features.commonfeatures.impl.addfunds.model.AddFundsModel
import com.tangem.features.commonfeatures.impl.addfunds.model.uiSpec
import com.tangem.features.commonfeatures.impl.managefunds.model.ManageFundsModel
import com.tangem.features.commonfeatures.impl.managefunds.model.uiSpec
import com.tangem.common.ui.markets.action.TokenActionsContext
import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.userportfolio.UserPortfolioComponent
import com.tangem.features.commonfeatures.impl.R
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(
internal class DefaultManageFundsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: AddFundsComponent.Params,
@Assisted private val params: ManageFundsComponent.Params,
chooseTokenComponentFactory: ChooseTokenComponent.Factory,
tokenActionsComponentFactory: TokenActionsComponent.Factory,
userPortfolioComponentFactory: UserPortfolioComponent.Factory,
walletFeatureToggles: WalletFeatureToggles,
) : AppComponentContext by appComponentContext, AddFundsComponent {
) : AppComponentContext by appComponentContext, ManageFundsComponent {
private val model: AddFundsModel = getOrCreateModel(params)
private val model: ManageFundsModel = getOrCreateModel(params)
private val isCompactTokenActions: Boolean = params.launchMode is AddFundsComponent.LaunchMode.TokenActionsOnly
private val isCompactTokenActions: Boolean = params.launchMode is ManageFundsComponent.LaunchMode.TokenActionsOnly
private val isAddFundsStage1Enabled: Boolean = walletFeatureToggles.isAddFundsStage1Enabled
private val tokenActionsComponent: TokenActionsComponent by lazy {
tokenActionsComponentFactory.create(
context = child(key = "addFundsTokenActions"),
context = child(key = "manageFundsTokenActions"),
params = TokenActionsComponent.Params(
data = model.tokenActionsData,
callbacks = model,
bottomAction = model.currentBottomAction,
isRedesignForced = true,
isCompact = isCompactTokenActions,
context = when (model.flowType) {
ManageFundsComponent.FlowType.AddFunds -> TokenActionsContext.AddFunds
ManageFundsComponent.FlowType.Transfer -> TokenActionsContext.Transfer
},
),
)
}
private val chooseTokenComponent: ChooseTokenComponent? by lazy {
(params.launchMode as? AddFundsComponent.LaunchMode.ChooseToken)?.let {
(params.launchMode as? ManageFundsComponent.LaunchMode.ChooseToken)?.let {
chooseTokenComponentFactory.create(
context = child(key = "addFundsChooseToken"),
context = child(key = "manageFundsChooseToken"),
params = ChooseTokenComponent.Params(
bridge = model.chooseTokenBridge,
),
@ -76,7 +81,7 @@ internal class DefaultAddFundsComponent @AssistedInject constructor(
private val userPortfolioComponent: UserPortfolioComponent by lazy {
userPortfolioComponentFactory.create(
context = child(key = "addFundsUserPortfolio"),
context = child(key = "manageFundsUserPortfolio"),
params = UserPortfolioComponent.Params(
uiState = model.userPortfolioStateController.uiState,
callbacks = object : UserPortfolioComponent.Callbacks {
@ -94,8 +99,8 @@ internal class DefaultAddFundsComponent @AssistedInject constructor(
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
if (route != ManageFundsModel.UiRoute.UserPortfolio) return@LaunchedEffect
val mode = params.launchMode as? ManageFundsComponent.LaunchMode.FilteredByRawId ?: return@LaunchedEffect
model.userPortfolioStateController.updateAndWaitNotNullState(
allAvailableData = model.buildAvailableToAddDataForChooser(),
rawCurrencyId = mode.rawCurrencyId,
@ -111,10 +116,10 @@ internal class DefaultAddFundsComponent @AssistedInject constructor(
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) {
is ManageFundsComponent.LaunchMode.TokenActionsOnly -> TangemBottomSheetType.Modal
is ManageFundsComponent.LaunchMode.ChooseToken -> TangemBottomSheetType.Default
is ManageFundsComponent.LaunchMode.FilteredByRawId ->
if (route is ManageFundsModel.UiRoute.TokenActions) {
TangemBottomSheetType.Default
} else {
TangemBottomSheetType.Modal
@ -122,7 +127,7 @@ internal class DefaultAddFundsComponent @AssistedInject constructor(
},
containerColor = TangemTheme.colors2.surface.level2,
title = {
AddFundsBottomSheetTitle(
ManageFundsBottomSheetTitle(
route = route,
canGoBack = canGoBack,
onBackClick = model::onBack,
@ -131,7 +136,7 @@ internal class DefaultAddFundsComponent @AssistedInject constructor(
},
content = {
val animatedContentModifier =
if (params.launchMode is AddFundsComponent.LaunchMode.ChooseToken) {
if (params.launchMode is ManageFundsComponent.LaunchMode.ChooseToken) {
Modifier.fillMaxSize()
} else {
Modifier
@ -139,11 +144,12 @@ internal class DefaultAddFundsComponent @AssistedInject constructor(
AnimatedContent(
targetState = route,
modifier = animatedContentModifier,
label = "AddFundsContentAnimation",
label = "ManageFundsContentAnimation",
) { animatedRoute ->
AddFundsRouteContent(
ManageFundsRouteContent(
route = animatedRoute,
shouldFillHeight = !isCompactTokenActions && animatedRoute.uiSpec().shouldFillHeight,
shouldFillHeight = !isCompactTokenActions &&
animatedRoute.uiSpec(model.flowType).shouldFillHeight,
)
}
},
@ -152,8 +158,8 @@ internal class DefaultAddFundsComponent @AssistedInject constructor(
}
@Composable
private fun AddFundsRouteContent(route: AddFundsModel.UiRoute, shouldFillHeight: Boolean) {
val spec = route.uiSpec()
private fun ManageFundsRouteContent(route: ManageFundsModel.UiRoute, shouldFillHeight: Boolean) {
val spec = route.uiSpec(model.flowType)
val horizontalPadding = if (spec.shouldApplyHorizontalPadding) {
Modifier.padding(horizontal = TangemTheme.dimens2.x4)
} else {
@ -164,33 +170,33 @@ internal class DefaultAddFundsComponent @AssistedInject constructor(
}
@Composable
private fun RenderRoute(route: AddFundsModel.UiRoute, modifier: Modifier = Modifier) {
private fun RenderRoute(route: ManageFundsModel.UiRoute, modifier: Modifier = Modifier) {
when (route) {
AddFundsModel.UiRoute.Loading -> Unit
AddFundsModel.UiRoute.ChooseToken -> chooseTokenComponent?.Content(modifier)
AddFundsModel.UiRoute.UserPortfolio -> CompositionLocalProvider(
ManageFundsModel.UiRoute.Loading -> Unit
ManageFundsModel.UiRoute.ChooseToken -> chooseTokenComponent?.Content(modifier)
ManageFundsModel.UiRoute.UserPortfolio -> CompositionLocalProvider(
LocalTangemBottomSheetContentBottomInset provides TangemTheme.dimens2.x4,
) {
userPortfolioComponent.Content(modifier)
}
AddFundsModel.UiRoute.TokenActions -> tokenActionsComponent.Content(modifier)
ManageFundsModel.UiRoute.TokenActions -> tokenActionsComponent.Content(modifier)
}
}
@Composable
private fun AddFundsBottomSheetTitle(
route: AddFundsModel.UiRoute,
private fun ManageFundsBottomSheetTitle(
route: ManageFundsModel.UiRoute,
canGoBack: Boolean,
onBackClick: () -> Unit,
onCloseClick: () -> Unit,
) {
TangemTopBar(
title = route.uiSpec().title,
title = route.uiSpec(model.flowType).title,
type = TangemTopBarType.BottomSheet,
startContent = if (canGoBack) {
{
TangemButton(
iconStart = TangemIconUM.Icon(iconRes = CoreR.drawable.ic_arrow_back_28),
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_arrow_back_28),
onClick = onBackClick,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
@ -202,7 +208,7 @@ internal class DefaultAddFundsComponent @AssistedInject constructor(
endContent = {
TangemButton(
modifier = Modifier.testTag(BaseBottomSheetTestTags.CLOSE_BUTTON),
iconStart = TangemIconUM.Icon(iconRes = CoreR.drawable.ic_close_24),
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24),
onClick = onCloseClick,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
@ -221,7 +227,10 @@ internal class DefaultAddFundsComponent @AssistedInject constructor(
}
@AssistedFactory
interface Factory : AddFundsComponent.Factory {
override fun create(context: AppComponentContext, params: AddFundsComponent.Params): DefaultAddFundsComponent
interface Factory : ManageFundsComponent.Factory {
override fun create(
context: AppComponentContext,
params: ManageFundsComponent.Params,
): DefaultManageFundsComponent
}
}

View file

@ -1,23 +1,23 @@
package com.tangem.features.commonfeatures.impl.addfunds.analytics
package com.tangem.features.commonfeatures.impl.managefunds.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
internal sealed class AddFundsAnalyticsEvent(
internal sealed class ManageFundsAnalyticsEvent(
event: String,
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category = CATEGORY, event = event, params = params) {
class MethodScreenOpened(source: String) : AddFundsAnalyticsEvent(
class MethodScreenOpened(source: String) : ManageFundsAnalyticsEvent(
event = "Method Screen Opened",
params = mapOf(AnalyticsParam.SOURCE to source),
)
class ButtonBuy : AddFundsAnalyticsEvent(event = "Button - Buy")
class ButtonBuy : ManageFundsAnalyticsEvent(event = "Button - Buy")
class ButtonSwap : AddFundsAnalyticsEvent(event = "Button - Swap")
class ButtonSwap : ManageFundsAnalyticsEvent(event = "Button - Swap")
class ButtonReceive : AddFundsAnalyticsEvent(event = "Button - Receive")
class ButtonReceive : ManageFundsAnalyticsEvent(event = "Button - Receive")
companion object {
private const val CATEGORY = "Add Funds"

View file

@ -0,0 +1,16 @@
package com.tangem.features.commonfeatures.impl.managefunds.di
import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent
import com.tangem.features.commonfeatures.impl.managefunds.DefaultManageFundsComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal interface ManageFundsComponentModule {
@Binds
fun bindManageFundsComponentFactory(factory: DefaultManageFundsComponent.Factory): ManageFundsComponent.Factory
}

View file

@ -1,8 +1,8 @@
package com.tangem.features.commonfeatures.impl.addfunds.di
package com.tangem.features.commonfeatures.impl.managefunds.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.commonfeatures.impl.addfunds.model.AddFundsModel
import com.tangem.features.commonfeatures.impl.managefunds.model.ManageFundsModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -11,10 +11,10 @@ import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface AddFundsModelModule {
internal interface ManageFundsModelModule {
@Binds
@IntoMap
@ClassKey(AddFundsModel::class)
fun addFundsModel(model: AddFundsModel): Model
@ClassKey(ManageFundsModel::class)
fun manageFundsModel(model: ManageFundsModel): Model
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.commonfeatures.impl.addfunds.model
package com.tangem.features.commonfeatures.impl.managefunds.model
import androidx.compose.runtime.Immutable
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
@ -19,14 +19,14 @@ 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.managefunds.ManageFundsComponent
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.managefunds.analytics.ManageFundsAnalyticsEvent
import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent
import com.tangem.features.commonfeatures.impl.userportfolio.state.UserPortfolioStateController
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -37,7 +37,7 @@ import javax.inject.Inject
@ModelScoped
@Suppress("LongParameterList")
internal class AddFundsModel @Inject constructor(
internal class ManageFundsModel @Inject constructor(
paramsContainer: ParamsContainer,
chooseTokenBridgeFactory: ChooseTokenBridge.Factory,
userPortfolioStateControllerFactory: UserPortfolioStateController.Factory,
@ -50,8 +50,9 @@ internal class AddFundsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
) : Model(), TokenActionsComponent.Callbacks {
private val params = paramsContainer.require<AddFundsComponent.Params>()
val launchMode: AddFundsComponent.LaunchMode = params.launchMode
private val params = paramsContainer.require<ManageFundsComponent.Params>()
val launchMode: ManageFundsComponent.LaunchMode = params.launchMode
val flowType: ManageFundsComponent.FlowType = params.flowType
private val routeStack = MutableStateFlow(listOf<UiRoute>(UiRoute.Loading))
@ -95,7 +96,10 @@ internal class AddFundsModel @Inject constructor(
val chooseTokenBridge: ChooseTokenBridge by lazy {
chooseTokenBridgeFactory.create(
modelScope = modelScope,
settings = ChooseTokenBridge.Settings.AddFunds,
settings = when (flowType) {
ManageFundsComponent.FlowType.AddFunds -> ChooseTokenBridge.Settings.AddFunds
ManageFundsComponent.FlowType.Transfer -> ChooseTokenBridge.Settings.Transfer
},
analyticsPayload = setOf(ChooseTokenAnalyticsPayload.ScreensSources(SCREEN_SOURCE)),
)
}
@ -116,9 +120,9 @@ internal class AddFundsModel @Inject constructor(
init {
when (val mode = launchMode) {
is AddFundsComponent.LaunchMode.ChooseToken -> initChooseToken(mode)
is AddFundsComponent.LaunchMode.TokenActionsOnly -> initTokenActionsOnly(mode)
is AddFundsComponent.LaunchMode.FilteredByRawId -> initFilteredByRawId(mode)
is ManageFundsComponent.LaunchMode.ChooseToken -> initChooseToken(mode)
is ManageFundsComponent.LaunchMode.TokenActionsOnly -> initTokenActionsOnly(mode)
is ManageFundsComponent.LaunchMode.FilteredByRawId -> initFilteredByRawId(mode)
}
}
@ -143,9 +147,9 @@ internal class AddFundsModel @Inject constructor(
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()
TokenActionsBSContentUM.Action.Buy -> ManageFundsAnalyticsEvent.ButtonBuy()
TokenActionsBSContentUM.Action.Exchange -> ManageFundsAnalyticsEvent.ButtonSwap()
TokenActionsBSContentUM.Action.Receive -> ManageFundsAnalyticsEvent.ButtonReceive()
else -> null
}
event?.let { analyticsEventHandler.send(it) }
@ -168,10 +172,10 @@ internal class AddFundsModel @Inject constructor(
)
}
private fun initChooseToken(mode: AddFundsComponent.LaunchMode.ChooseToken) {
private fun initChooseToken(mode: ManageFundsComponent.LaunchMode.ChooseToken) {
chooseTokenBridge.selectWalletTab(mode.userWalletId)
analyticsEventHandler.send(
AddFundsAnalyticsEvent.MethodScreenOpened(source = AddFundsAnalyticsEvent.SOURCE_MAIN_SCREEN),
ManageFundsAnalyticsEvent.MethodScreenOpened(source = ManageFundsAnalyticsEvent.SOURCE_MAIN_SCREEN),
)
replaceRoot(UiRoute.ChooseToken)
modelScope.launch {
@ -182,7 +186,7 @@ internal class AddFundsModel @Inject constructor(
}
}
private fun initTokenActionsOnly(mode: AddFundsComponent.LaunchMode.TokenActionsOnly) {
private fun initTokenActionsOnly(mode: ManageFundsComponent.LaunchMode.TokenActionsOnly) {
modelScope.launch {
val wallet = getUserWalletUseCase.invokeFlow(mode.userWalletId)
.mapNotNull { it.getOrNull() }
@ -206,7 +210,7 @@ internal class AddFundsModel @Inject constructor(
}
}
private fun initFilteredByRawId(mode: AddFundsComponent.LaunchMode.FilteredByRawId) {
private fun initFilteredByRawId(mode: ManageFundsComponent.LaunchMode.FilteredByRawId) {
modelScope.launch {
val entries = collectFilteredEntries(mode.rawCurrencyId)
when (entries.size) {

View file

@ -0,0 +1,38 @@
package com.tangem.features.commonfeatures.impl.managefunds.model
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent
import com.tangem.features.commonfeatures.impl.R
internal data class ManageFundsRouteUiSpec(
val title: TextReference,
val shouldApplyHorizontalPadding: Boolean,
val shouldFillHeight: Boolean,
)
internal fun ManageFundsModel.UiRoute.uiSpec(flowType: ManageFundsComponent.FlowType): ManageFundsRouteUiSpec {
val isTransfer = flowType == ManageFundsComponent.FlowType.Transfer
return when (this) {
ManageFundsModel.UiRoute.Loading -> ManageFundsRouteUiSpec(
title = resourceReference(if (isTransfer) R.string.common_choose_token else R.string.common_add_funds),
shouldApplyHorizontalPadding = false,
shouldFillHeight = false,
)
ManageFundsModel.UiRoute.ChooseToken -> ManageFundsRouteUiSpec(
title = resourceReference(R.string.common_choose_token),
shouldApplyHorizontalPadding = false,
shouldFillHeight = true,
)
ManageFundsModel.UiRoute.UserPortfolio -> ManageFundsRouteUiSpec(
title = resourceReference(R.string.common_add_funds),
shouldApplyHorizontalPadding = false,
shouldFillHeight = false,
)
ManageFundsModel.UiRoute.TokenActions -> ManageFundsRouteUiSpec(
title = resourceReference(if (isTransfer) R.string.common_transfer else R.string.common_get_token),
shouldApplyHorizontalPadding = true,
shouldFillHeight = true,
)
}
}

View file

@ -10,6 +10,7 @@ import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.common.ui.markets.action.CryptoCurrencyData
import com.tangem.common.ui.markets.action.TokenActionsBSContentUM
import com.tangem.common.ui.markets.action.TokenActionsContext
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.factory.ComponentFactory
@ -79,6 +80,7 @@ internal class TokenActionsComponent @AssistedInject constructor(
val bottomAction: Flow<BottomAction> = flowOf(BottomAction.None),
val isRedesignForced: Boolean = false,
val isCompact: Boolean = false,
val context: TokenActionsContext = TokenActionsContext.Markets,
)
interface Callbacks {

View file

@ -92,6 +92,7 @@ internal class TokenActionsUiBuilder @Inject constructor(
cryptoData = cryptoCurrencyData,
tokenActionsHandler = tokenActionsHandler,
isRedesignEnabled = false,
context = params.context,
),
bottomActionText = bottomActionText(bottomAction),
onBottomActionClick = {
@ -124,6 +125,7 @@ internal class TokenActionsUiBuilder @Inject constructor(
cryptoData = cryptoCurrencyData,
tokenActionsHandler = tokenActionsHandler,
isRedesignEnabled = true,
context = params.context,
),
bottomActionText = bottomActionText(bottomAction),
onBottomActionClick = {

View file

@ -8,6 +8,7 @@ import com.tangem.core.decompose.navigation.Route
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent
import com.tangem.features.feed.components.earn.DefaultEarnComponent
import com.tangem.features.feed.components.feed.DefaultFeedComponent
import com.tangem.features.feed.components.feed.DefaultFeedComponent.FeedParams
@ -29,7 +30,7 @@ internal class FeedEntryChildFactory @Inject constructor(
private val portfolioComponentFactory: MarketsPortfolioComponent.Factory,
private val portfolioBlockComponentFactory: PortfolioBlockComponent.Factory,
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
private val addFundsComponentFactory: com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent.Factory,
private val manageFundsComponentFactory: ManageFundsComponent.Factory,
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
private val designFeatureToggles: DesignFeatureToggles,
) {
@ -84,7 +85,7 @@ internal class FeedEntryChildFactory @Inject constructor(
portfolioBlockComponentFactory = portfolioBlockComponentFactory,
designFeatureToggles = designFeatureToggles,
addToPortfolioComponentFactory = addToPortfolioComponentFactory,
addFundsComponentFactory = addFundsComponentFactory,
manageFundsComponentFactory = manageFundsComponentFactory,
)
}
is Child.TokenList -> {

View file

@ -42,7 +42,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PreselectedTokenDetailsSection
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent
import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent
@ -66,7 +66,7 @@ internal class DefaultMarketsTokenDetailsComponent(
portfolioBlockComponentFactory: PortfolioBlockComponent.Factory,
val params: Params,
private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory,
private val addFundsComponentFactory: AddFundsComponent.Factory,
private val manageFundsComponentFactory: ManageFundsComponent.Factory,
) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext {
// applying l2 compatibility
@ -174,10 +174,10 @@ internal class DefaultMarketsTokenDetailsComponent(
config: AddFundsSlotRoute,
componentContext: ComponentContext,
): ComposableBottomSheetComponent {
val launchMode = AddFundsComponent.LaunchMode.FilteredByRawId(rawCurrencyId = config.rawCurrencyId)
return addFundsComponentFactory.create(
val launchMode = ManageFundsComponent.LaunchMode.FilteredByRawId(rawCurrencyId = config.rawCurrencyId)
return manageFundsComponentFactory.create(
context = childByContext(componentContext),
params = AddFundsComponent.Params(
params = ManageFundsComponent.Params(
launchMode = launchMode,
onDismiss = { model.addFundsSheetNavigation.dismiss() },
),

View file

@ -3,17 +3,18 @@ package com.tangem.features.feed.ui.utils
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import com.arkivanov.decompose.Child
import com.arkivanov.decompose.FaultyDecomposeApi
import com.arkivanov.decompose.extensions.compose.stack.animation.StackAnimation
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.stack.animation.*
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.features.feed.components.FeedEntryChildFactory
import kotlin.math.abs
private const val FEED_ENTRY_SLIDE_DURATION_MS = 300
private const val FEED_ENTRY_FADE_DURATION_MS = 300
internal typealias FeedEntryActiveChild =
Child.Created<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>
@ -21,19 +22,33 @@ internal typealias FeedEntryActiveChild =
internal typealias FeedEntryChildStack =
ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>
@OptIn(FaultyDecomposeApi::class)
// Single-child `stackAnimation` overload (-> SimpleStackAnimation), like the root content. Avoids the
// 3-arg overload, which is @FaultyDecomposeApi and backed by a movableContentOf impl known to misbehave on
// rapid transition interruptions (the "stuck content" bug). The selector sees only one child, so the fade vs
// slide choice is per-screen: Search fades, everything else slides.
internal fun contentFeedEntryStackAnimation(): StackAnimation<
FeedEntryChildFactory.Child,
ComposableModularBottomSheetContentComponent,
> =
stackAnimation { to, from, _ ->
if (to.configuration.usesFadeStackTransition() || from.configuration.usesFadeStackTransition()) {
fade()
stackAnimation { child ->
if (child.configuration.usesFadeStackTransition()) {
feedContentFade()
} else {
slide()
}
}
private fun feedContentFade(): StackAnimator = stackAnimator(
animationSpec = tween(FEED_ENTRY_FADE_DURATION_MS),
) { factor, _, content ->
content(
Modifier.graphicsLayer {
alpha = 1f - abs(factor)
compositingStrategy = CompositingStrategy.ModulateAlpha
},
)
}
internal fun topBarFeedEntryAnimatedContentTransitionSpec(
stackState: State<FeedEntryChildStack>,
): AnimatedContentTransitionScope<FeedEntryActiveChild>.() -> ContentTransform =

View file

@ -10,6 +10,7 @@ interface SendEntryPointComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val shouldStartWithSwap: Boolean = false,
)
interface Factory : ComponentFactory<Params, SendEntryPointComponent>

View file

@ -98,6 +98,11 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor(
model.currentRoute.emit(stack.active.configuration)
}
}
if (params.shouldStartWithSwap) {
// Direct Swap&Send entry: replicate the canonical "send with swap" trigger so the receive-token
// selector opens like the regular Send -> Send&Swap flow (this was the less-broken behaviour).
model.onConvertToAnotherToken(lastAmount = "", isEnterInFiatSelected = false)
}
}
@Composable

View file

@ -565,12 +565,11 @@ internal class SwapAmountModel @Inject constructor(
(params as? SwapAmountComponentParams.AmountParams)?.callback?.resetSendWithSwapNavigation(
data.shouldResetNavigation,
)
ensurePrimaryReady()
initPairs(data.swapCurrencies, data.cryptoCurrency)
uiState.update { amountUM ->
if (amountUM is SwapAmountUM.Content) {
initPairs(data.swapCurrencies, data.cryptoCurrency)
amountUM.copy(
isPrimaryButtonEnabled = false,
)
amountUM.copy(isPrimaryButtonEnabled = false)
} else {
amountUM
}
@ -579,6 +578,32 @@ internal class SwapAmountModel @Inject constructor(
.launchIn(modelScope)
}
/**
* Forces the amount UI into the primary-ready [SwapAmountUM.Content] state using the current primary
* status. Needed for the direct Send-with-Swap (Swap&Send) entry where the state can be [SwapAmountUM.Empty] and
* the balance-`distinctUntilChanged` primary status flow won't re-emit to rebuild it. No-op when already Content.
*/
private suspend fun ensurePrimaryReady() {
if (uiState.value is SwapAmountUM.Content) return
val primaryCurrencyStatus = params.primaryCryptoCurrencyStatusFlow.value
initCurrencies(primaryStatus = primaryCurrencyStatus, secondaryStatus = null)
val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1
uiState.transformerUpdate(
SwapAmountPrimaryReadyStateTransformer(
userWallet = userWallet,
primaryCryptoCurrencyStatus = primaryCurrencyStatus,
appCurrency = appCurrency,
swapDirection = swapDirection,
clickIntents = this,
isBalanceHidden = params.isBalanceHidingFlow.value,
isShowBestRateAnimation = isShowBestRateAnimation,
isSingleWallet = isOnlyOneWallet,
isAccountsMode = params.isAccountModeFlow.value,
account = params.accountFlow.value,
),
)
}
private fun initPairs(swapCurrencies: SwapCurrencies, secondaryCryptoCurrency: CryptoCurrency?) {
modelScope.launch {
val secondaryCurrency = selectInitialPairUseCase(

View file

@ -43,6 +43,7 @@ dependencies {
implementation(projects.domain.transaction.models)
implementation(projects.domain.express.models)
implementation(projects.domain.account.status)
implementation(projects.domain.txhistory)
implementation(projects.libs.blockchainSdk)
implementation(projects.libs.crypto)

View file

@ -28,6 +28,7 @@ import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.txhistory.TxHistoryFeatureToggles
import com.tangem.feature.swap.converters.*
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.ExpressDataError
@ -50,6 +51,7 @@ internal class DefaultSwapRepository(
private val dataSignatureVerifier: DataSignatureVerifier,
private val appPreferencesStore: AppPreferencesStore,
private val expressHistoryDao: ExpressHistoryDao,
private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
moshi: Moshi,
) : SwapRepository {
@ -156,7 +158,9 @@ internal class DefaultSwapRepository(
.getOrThrow()
val entity = response.toEntity(ownerAddress = response.fromAddress.orEmpty())
expressHistoryDao.upsertExchanges(listOf(entity))
if (txHistoryFeatureToggles.isNewTxHistoryEnabled) {
expressHistoryDao.upsertExchanges(listOf(entity))
}
exchangeStatusConverter.convert(response)
},

View file

@ -12,6 +12,7 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.txhistory.TxHistoryFeatureToggles
import com.tangem.feature.swap.DefaultSwapFeedbackRepository
import com.tangem.feature.swap.DefaultSwapRepository
import com.tangem.feature.swap.NoOpSwapFeedbackRepository
@ -41,6 +42,7 @@ internal class SwapDataModule {
@NetworkMoshi moshi: Moshi,
appPreferencesStore: AppPreferencesStore,
expressHistoryDao: ExpressHistoryDao,
txHistoryFeatureToggles: TxHistoryFeatureToggles,
): SwapRepository {
return DefaultSwapRepository(
tangemExpressApi = tangemExpressApi,
@ -50,6 +52,7 @@ internal class SwapDataModule {
moshi = moshi,
appPreferencesStore = appPreferencesStore,
expressHistoryDao = expressHistoryDao,
txHistoryFeatureToggles = txHistoryFeatureToggles,
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.swap.model
import com.google.common.truth.Truth.assertThat
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
@ -41,7 +42,7 @@ internal class SwapModelGetSelectApprovalTypeParamsTest : SwapModelTestBase() {
@Test
fun `GIVEN provider state is not QuotesLoadedState THEN returns null`() {
val provider = swapProvider()
val notLoaded: SwapState.EmptyAmountState = mockk(relaxed = true)
val notLoaded = SwapState.EmptyAmountState(zeroAmountEquivalent = TextReference.EMPTY)
val model = createModel()
model.dataState = model.dataState.copy(
fromSwapCurrencyStatus = swapCurrencyStatus(),

View file

@ -11,6 +11,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_document_20
import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.pay.TangemPayCard
import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.models.pay.isFrozen
@ -59,6 +60,8 @@ internal class TangemPayDetailsStateFactory(
fun getLoadedState(status: PaymentAccountStatusValue.Loaded): TangemPayDetailsUM {
val hasUnfrozenCard = status.cards.any { it.frozenState == TangemPayCardFrozenState.Unfrozen }
val hasIssuingCard = status.cards.any { it.state == TangemPayCardState.Issuing }
val isAddCardEnabled = status.error == null && !hasIssuingCard
return TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig(
onBackClick = onBack,
@ -79,16 +82,15 @@ internal class TangemPayDetailsStateFactory(
TangemPayDetailsBalanceBlockState.Card(
lastDigits = cardItem.lastDigits,
onClick = { intents.onCardClick(cardItem.id) },
isReissuingOrClosing = cardItem.state == TangemPayCardState.Reissuing ||
cardItem.state == TangemPayCardState.Closing,
isEnabled = status.error == null,
isFrozen = cardItem.isFrozen,
isIssuing = cardItem.state == TangemPayCardState.Issuing,
state = cardItem.state.toUiState(),
)
}
.toImmutableList(),
onAddCardClick = intents::onAddCardClick,
isAddCardEnabled = status.error == null,
isAddCardEnabled = isAddCardEnabled,
progressBanner = status.cards.resolveProgressBanner(),
),
),
isBalanceHidden = false,
@ -102,6 +104,12 @@ internal class TangemPayDetailsStateFactory(
)
}
private fun List<TangemPayCard>.resolveProgressBanner(): CardsProgressBannerUM? = when {
any { it.state == TangemPayCardState.Reissuing } -> CardsProgressBannerUM.Reissuing
any { it.state == TangemPayCardState.Issuing } -> CardsProgressBannerUM.Issuing
else -> null
}
fun getDeactivatedState(): TangemPayDetailsUM {
return TangemPayDetailsUM(
topBarConfig = TangemPayDetailsTopBarConfig(

View file

@ -7,6 +7,7 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState
import kotlinx.collections.immutable.ImmutableList
internal enum class CardDataType {
@ -94,13 +95,13 @@ internal sealed class TangemPayDetailsBalanceBlockState {
val cards: ImmutableList<Card>,
val onAddCardClick: () -> Unit,
val isAddCardEnabled: Boolean,
val progressBanner: CardsProgressBannerUM? = null,
)
data class Card(
val lastDigits: String,
val onClick: () -> Unit,
val isReissuingOrClosing: Boolean,
val isIssuing: Boolean,
val state: TangemPayCardUiState,
val isFrozen: Boolean,
val isEnabled: Boolean,
)
@ -109,4 +110,22 @@ internal sealed class TangemPayDetailsBalanceBlockState {
internal data class AddToWalletBlockState(
val onClick: () -> Unit,
val onClickClose: () -> Unit,
)
)
internal enum class TangemPayCardUiState {
Active,
InProgress,
}
internal enum class CardsProgressBannerUM {
Issuing,
Reissuing,
}
internal fun TangemPayCardState.toUiState(): TangemPayCardUiState = when (this) {
TangemPayCardState.Active -> TangemPayCardUiState.Active
TangemPayCardState.Issuing,
TangemPayCardState.Reissuing,
TangemPayCardState.Closing,
-> TangemPayCardUiState.InProgress
}

View file

@ -26,6 +26,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
@ -37,12 +39,14 @@ import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.constraintlayout.compose.ConstrainedLayoutReference
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.ConstraintLayoutScope
import androidx.constraintlayout.compose.Dimension
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
@ -60,9 +64,12 @@ import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.CardDataType
import com.tangem.features.tangempay.entity.DisplayNameState
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
import kotlin.math.roundToInt
private const val TEXT_WIDTH_PADDING = 2
private const val FREEZE_ANIMATION_DURATION_MS = 600
private const val CARD_WIDTH_RATIO = 328f
private const val CARD_HEIGHT_RATIO = 212f
private val CustomCardBlockColor = Color(0x1F828282)
private val CardBackgroundColor = Color(0xFF171A27)
@ -86,9 +93,10 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M
CardBgWrapper(
rotateCardY = rotateCardY,
zAxisDistance = zAxisDistance,
shouldShowDetails = shouldShowDetails,
modifier = modifier,
) {
if (shouldShowDetails) {
front = { TangemPayCardDetailsHiddenBlock(state = state) },
back = {
TangemPayCardDetailsShownBlock(
cardNumber = state.number,
expiry = state.expiry,
@ -99,10 +107,8 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M
onHideDetails = state.onClick,
modifier = Modifier.graphicsLayer { rotationY = 180f },
)
} else {
TangemPayCardDetailsHiddenBlock(state = state)
}
}
},
)
}
@Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries")
@ -111,7 +117,7 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
Box(modifier = modifier.fillMaxSize()) {
TangemPayCardBackground(
modifier = Modifier
.fillMaxSize()
.matchParentSize()
.zIndex(0f),
cardFrozenState = state.cardFrozenState,
)
@ -219,6 +225,7 @@ private fun TangemPayCardBackground(cardFrozenState: TangemPayCardFrozenState, m
modifier = Modifier.fillMaxSize(),
painter = painterResource(R.drawable.img_tangem_pay_visa),
contentDescription = null,
contentScale = ContentScale.FillBounds,
)
if (isFrozen || freezeProgress > 0f) {
@ -228,25 +235,27 @@ private fun TangemPayCardBackground(cardFrozenState: TangemPayCardFrozenState, m
.graphicsLayer { alpha = freezeProgress },
painter = painterResource(R.drawable.img_tangem_pay_visa_frozen),
contentDescription = null,
contentScale = ContentScale.Crop,
)
}
}
}
@Suppress("MagicNumber")
@Suppress("MagicNumber", "LongMethod")
@Composable
private fun CardBgWrapper(
rotateCardY: Float,
zAxisDistance: Float,
shouldShowDetails: Boolean,
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit,
back: @Composable () -> Unit,
front: @Composable () -> Unit,
) {
val isRedesignEnabled = LocalVisaRedesignEnabled.current
val shouldShowDetailsBg = rotateCardY > 90f && isRedesignEnabled
val shouldShowDetailsBg = shouldShowDetails && isRedesignEnabled
Box(
modifier = modifier
.fillMaxWidth()
.aspectRatio(328f / 212f) // size of img_tangem_pay_visa
.graphicsLayer {
rotationY = rotateCardY
cameraDistance = zAxisDistance
@ -278,24 +287,77 @@ private fun CardBgWrapper(
},
),
) {
Box(modifier = Modifier.fillMaxWidth()) {
if (shouldShowDetailsBg) {
Image(
EqualHeightCardSides(
modifier = Modifier.fillMaxWidth(),
placeBackOnTop = shouldShowDetails,
front = {
Box(
modifier = Modifier
.fillMaxSize()
.graphicsLayer {
rotationY = rotateCardY
cameraDistance = zAxisDistance
},
painter = painterResource(R.drawable.img_bg_card_details),
contentDescription = null,
)
.graphicsLayer { alpha = if (shouldShowDetails) 0f else 1f },
) { front() }
},
back = {
Box(
modifier = Modifier
.fillMaxSize()
.graphicsLayer { alpha = if (shouldShowDetails) 1f else 0f },
) {
if (shouldShowDetailsBg) {
Image(
modifier = Modifier
.matchParentSize()
.graphicsLayer {
rotationY = rotateCardY
cameraDistance = zAxisDistance
},
painter = painterResource(R.drawable.img_bg_card_details),
contentDescription = null,
contentScale = ContentScale.Crop,
)
}
back()
}
},
)
}
}
@Composable
private fun EqualHeightCardSides(
placeBackOnTop: Boolean,
modifier: Modifier = Modifier,
back: @Composable () -> Unit,
front: @Composable () -> Unit,
) {
SubcomposeLayout(modifier) { constraints ->
val width = constraints.maxWidth
val minHeight = if (width == Constraints.Infinity) {
0
} else {
(width * CARD_HEIGHT_RATIO / CARD_WIDTH_RATIO).roundToInt()
}
val naturalConstraints = constraints.copy(minHeight = 0, maxHeight = Constraints.Infinity)
val backNaturalHeight = subcompose(CardSide.BackMeasure, back)
.maxOfOrNull { it.measure(naturalConstraints).height } ?: 0
val finalHeight = maxOf(minHeight, backNaturalHeight)
val sizeConstraints = constraints.copy(minHeight = finalHeight, maxHeight = finalHeight)
val frontPlaceables = subcompose(CardSide.Front, front).map { it.measure(sizeConstraints) }
val backPlaceables = subcompose(CardSide.Back, back).map { it.measure(sizeConstraints) }
layout(width, finalHeight) {
val ordered = if (placeBackOnTop) {
frontPlaceables + backPlaceables
} else {
backPlaceables + frontPlaceables
}
content()
ordered.forEach { it.place(0, 0) }
}
}
}
private enum class CardSide { BackMeasure, Front, Back }
@Composable
private fun CardTopBlock(modifier: Modifier = Modifier) {
Row(
@ -488,7 +550,7 @@ private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Mo
LaunchedEffect(Unit) { focusRequester.requestFocus() }
}
@Suppress("MagicNumber", "LongParameterList")
@Suppress("MagicNumber", "LongParameterList", "LongMethod")
@Composable
private fun TangemPayCardDetailsShownBlock(
cardNumber: String,
@ -538,6 +600,7 @@ private fun TangemPayCardDetailsShownBlock(
copyTestTag = TangemPayTestTags.CARD_DETAILS_COPY_CVC,
)
}
SpacerH(8.dp)
Spacer(modifier = Modifier.weight(1f))
Row {
SpacerWMax()

View file

@ -97,8 +97,6 @@ private fun TangemPayCardPageScreen(
.fillMaxSize()
.padding(scaffoldPaddings),
contentPadding = PaddingValues(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16 + bottomBarHeight,
),
verticalArrangement = Arrangement.spacedBy(if (isRedesignEnabled) 0.dp else TangemTheme.dimens.spacing16),
@ -136,7 +134,10 @@ private fun TangemPayCardSwipePager(
) {
when {
controllers.isEmpty() -> Unit
controllers.size == 1 -> CardDetailsPage(controller = controllers.first(), modifier = modifier)
controllers.size == 1 -> CardDetailsPage(
controller = controllers.first(),
modifier = modifier.padding(horizontal = 16.dp),
)
else -> {
val initialPage = controllers.indexOfFirst { it.cardId == selectedCardId }.coerceAtLeast(0)
val pagerState = rememberPagerState(initialPage = initialPage) { controllers.size }
@ -328,7 +329,9 @@ private fun LazyListScope.cardPageItem(
enter = fadeIn(animationSpec = tween(CONTENT_FADE_DURATION_MS)),
exit = fadeOut(animationSpec = tween(CONTENT_FADE_DURATION_MS)),
) {
content()
Box(modifier = Modifier.padding(horizontal = 16.dp)) {
content()
}
}
}
}
@ -340,7 +343,12 @@ private fun TangemPayCardPageScreenPreviewV1() {
TangemThemePreview {
TangemPayCardPageScreen(
state = TangemPayCardPageUM.stub(),
cardSection = { TangemPayCard(state = previewCardDetailsState()) },
cardSection = {
TangemPayCard(
state = previewCardDetailsState(),
modifier = Modifier.padding(horizontal = 16.dp),
)
},
)
}
}
@ -371,7 +379,12 @@ private fun TangemPayCardPageScreenPreviewV2() {
) {
TangemPayCardPageScreen(
state = TangemPayCardPageUM.stub(),
cardSection = { TangemPayCard(state = previewCardDetailsState()) },
cardSection = {
TangemPayCard(
state = previewCardDetailsState(),
modifier = Modifier.padding(horizontal = 16.dp),
)
},
)
}
}

View file

@ -33,7 +33,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.RectangleShimmer
@ -132,7 +131,8 @@ internal fun TangemPayDetailsScreen(
},
)
if (state.balanceBlockState.cardsBlockState?.cards?.fastAny { it.isReissuingOrClosing } == true) {
val progressBanner = state.balanceBlockState.cardsBlockState?.progressBanner
if (progressBanner == CardsProgressBannerUM.Reissuing) {
item(
key = "REISSUE_MESSAGE",
content = {
@ -315,6 +315,7 @@ private fun CardsBlockRow(
@Composable
private fun TangemPayCardItem(card: TangemPayDetailsBalanceBlockState.Card, modifier: Modifier = Modifier) {
val isCardInProcess = card.state == TangemPayCardUiState.InProgress
Box(
modifier = modifier
.clip(RoundedCornerShape(4.dp))
@ -329,7 +330,7 @@ private fun TangemPayCardItem(card: TangemPayDetailsBalanceBlockState.Card, modi
.alpha(if (card.isEnabled) 1f else DISABLED_ALPHA)
.fillMaxSize(),
painter = painterResource(
if (card.isReissuingOrClosing) {
if (isCardInProcess) {
R.drawable.img_visa_card_inactive_48_32
} else {
R.drawable.img_visa_card_48_32
@ -337,7 +338,7 @@ private fun TangemPayCardItem(card: TangemPayDetailsBalanceBlockState.Card, modi
),
contentDescription = null,
)
if (!card.isReissuingOrClosing) {
if (!isCardInProcess) {
Text(
modifier = Modifier
.align(Alignment.BottomStart)
@ -486,18 +487,16 @@ internal class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<T
TangemPayDetailsBalanceBlockState.Card(
lastDigits = "1234",
onClick = {},
isReissuingOrClosing = false,
isEnabled = false,
isFrozen = false,
isIssuing = false,
state = TangemPayCardUiState.Active,
),
TangemPayDetailsBalanceBlockState.Card(
lastDigits = "3456",
onClick = {},
isReissuingOrClosing = false,
isEnabled = true,
isFrozen = false,
isIssuing = false,
state = TangemPayCardUiState.Active,
),
),
onAddCardClick = {},
@ -527,14 +526,14 @@ internal class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<T
TangemPayDetailsBalanceBlockState.Card(
lastDigits = "1234",
onClick = {},
isReissuingOrClosing = true,
isFrozen = false,
isEnabled = true,
isIssuing = false,
state = TangemPayCardUiState.InProgress,
),
),
onAddCardClick = {},
isAddCardEnabled = true,
progressBanner = CardsProgressBannerUM.Reissuing,
),
),
isBalanceHidden = false,

View file

@ -28,7 +28,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.SpacerW
@ -56,6 +55,8 @@ import com.tangem.features.tangempay.components.express.PreviewEmptyExpressTrans
import com.tangem.features.tangempay.components.txHistory.PreviewTangemPayTxHistoryComponent
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.CardsProgressBannerUM
import com.tangem.features.tangempay.entity.TangemPayCardUiState
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarConfig
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
@ -141,6 +142,7 @@ internal fun TangemPayDetailsScreenV2(
expressTransactionsBottomSheetState?.content(null)
}
@Suppress("LongMethod")
private fun LazyListScope.payDetailsBody(state: TangemPayDetailsUM) {
item("balanceBlock") {
BalanceBlock(
@ -158,8 +160,20 @@ private fun LazyListScope.payDetailsBody(state: TangemPayDetailsUM) {
ActionBlock(actionButtons = state.balanceBlockState.actionButtons)
}
}
when {
state.balanceBlockState.cardsBlockState?.cards?.fastAny { it.isReissuingOrClosing } == true -> {
if (state.errorNotificationConfig != null) {
item("errorSessionBannerBlock") {
ErrorMessage(
config = state.errorNotificationConfig,
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4),
)
}
}
val progressBanner = state.balanceBlockState.cardsBlockState?.progressBanner
when (progressBanner) {
CardsProgressBannerUM.Reissuing -> {
item("reissuingBannerBlock") {
TangemMessage(
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4),
@ -168,24 +182,25 @@ private fun LazyListScope.payDetailsBody(state: TangemPayDetailsUM) {
)
}
}
state.balanceBlockState.cardsBlockState?.cards?.fastAny { it.isIssuing } == true -> {
CardsProgressBannerUM.Issuing -> {
item("issuingBannerBlock") {
TangemMessage(
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4),
title = resourceReference(R.string.tangempay_issuing_new_digital_card_title),
subtitle = resourceReference(R.string.tangempay_reissue_card_in_progress_description),
contentColor = TangemTheme.colors3.bg.opaque.secondary,
leadingContent = {
Icon(
modifier = Modifier.size(20.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_clock_24),
contentDescription = null,
tint = TangemTheme.colors3.icon.primary,
)
},
)
}
}
else -> {
if (state.errorNotificationConfig != null) {
item("errorSessionBannerBlock") {
ErrorMessage(
config = state.errorNotificationConfig,
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4),
)
}
}
null -> {
if (state.addToWalletBlockState != null) {
item("addToWalletBannerBlock") {
TangemPayAddToWalletBlock(
@ -375,7 +390,7 @@ private fun CardsBlock(
) {
items(items = cardsBlockState.cards) { item ->
TangemPayCardView(
isIssueInProgress = item.isIssuing || item.isReissuingOrClosing,
isIssueInProgress = item.state != TangemPayCardUiState.Active,
lastDigits = item.lastDigits,
onClick = item.onClick,
isEnabled = item.isEnabled,

View file

@ -1,9 +1,11 @@
package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -11,7 +13,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -37,7 +39,6 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_card_plus_32
import com.tangem.core.ui.res.generated.icons.ic_error_28
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayIssueAdditionalCardUM
@ -72,47 +73,55 @@ internal fun TangemPayIssueAdditionalCardContent(state: TangemPayIssueAdditional
@Composable
private fun Content(state: TangemPayIssueAdditionalCardUM) {
val appearance = state.contentAppearance()
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerH(16.dp)
StatusIcon(appearance = appearance)
SpacerH(32.dp)
CenteredMessageText(
textRes = appearance.titleRes,
style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary,
)
CenteredMessageText(
textRes = appearance.subtitleRes,
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.secondary,
)
SpacerH(32.dp)
FeeBlock(modifier = Modifier.padding(top = 16.dp), state = state)
SpacerH(8.dp)
BottomButtonsBlock(state = state, appearance = appearance)
Column(modifier = Modifier.fillMaxWidth()) {
Header()
FeeBlock(state = state)
if (state.isBalanceInsufficient) {
InsufficientFundsNotification(state = state)
}
IssueButton(state = state)
}
}
@Composable
private fun StatusIcon(appearance: IssueAdditionalCardContentAppearance) {
private fun Header() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerH(32.dp)
StatusIcon()
SpacerH(32.dp)
CenteredMessageText(
textRes = R.string.tangempay_issue_additional_card_title,
style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary,
)
SpacerH(8.dp)
CenteredMessageText(
textRes = R.string.tangempay_issue_additional_card_description,
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.secondary,
)
SpacerH(32.dp)
}
}
@Composable
private fun StatusIcon() {
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(appearance.iconBackgroundColor),
.background(TangemTheme.colors3.bg.status.infoSubtle),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = appearance.icon,
imageVector = Icons.ic_card_plus_32,
contentDescription = null,
tint = appearance.iconColor,
tint = TangemTheme.colors3.icon.status.info,
modifier = Modifier.size(28.dp),
)
}
@ -152,68 +161,64 @@ private fun FeeBlock(state: TangemPayIssueAdditionalCardUM, modifier: Modifier =
}
@Composable
private fun BottomButtonsBlock(
state: TangemPayIssueAdditionalCardUM,
appearance: IssueAdditionalCardContentAppearance,
modifier: Modifier = Modifier,
) {
private fun InsufficientFundsNotification(state: TangemPayIssueAdditionalCardUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(top = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 16.dp)
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors3.bg.status.warningSubtle)
.padding(horizontal = 14.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.Top,
) {
Image(
modifier = Modifier.size(20.dp),
painter = painterResource(R.drawable.img_usdc_16),
contentDescription = null,
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = stringResourceSafe(R.string.tangempay_reissue_card_insufficient_funds_title),
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.primary,
)
Text(
text = stringResourceSafe(R.string.tangempay_reissue_card_insufficient_funds_subtitle),
style = TangemTheme.typography3.caption.medium,
color = TangemTheme.colors3.text.secondary,
)
}
}
TangemButton(
modifier = Modifier.fillMaxWidth(),
variant = TangemButton.Variant.Secondary,
size = TangemButton.Size.X12,
onClick = state.onDismiss,
text = resourceReference(R.string.common_cancel),
)
TangemButton(
modifier = Modifier.fillMaxWidth(),
size = TangemButton.Size.X12,
onClick = appearance.primaryAction(state),
isEnabled = !state.isLoading,
isLoading = state.isLoading,
text = resourceReference(appearance.primaryButtonTextRes),
size = TangemButton.Size.X8,
onClick = state.onAddFundsClick,
text = resourceReference(R.string.tangempay_card_details_add_funds),
)
}
}
private data class IssueAdditionalCardContentAppearance(
val titleRes: Int,
val subtitleRes: Int,
val icon: ImageVector,
val iconColor: Color,
val iconBackgroundColor: Color,
val primaryButtonTextRes: Int,
val primaryAction: (TangemPayIssueAdditionalCardUM) -> () -> Unit,
)
@Composable
private fun TangemPayIssueAdditionalCardUM.contentAppearance(): IssueAdditionalCardContentAppearance {
return if (isBalanceInsufficient) {
IssueAdditionalCardContentAppearance(
titleRes = R.string.tangempay_reissue_card_insufficient_funds_title,
subtitleRes = R.string.tangempay_reissue_card_insufficient_funds_subtitle,
icon = Icons.ic_error_28,
iconColor = TangemTheme.colors3.icon.status.warning,
iconBackgroundColor = TangemTheme.colors3.bg.status.warningSubtle,
primaryButtonTextRes = R.string.tangempay_card_details_add_funds,
primaryAction = { it.onAddFundsClick },
)
} else {
IssueAdditionalCardContentAppearance(
titleRes = R.string.tangempay_issue_additional_card_title,
subtitleRes = R.string.tangempay_issue_additional_card_description,
icon = Icons.ic_card_plus_32,
iconColor = TangemTheme.colors3.icon.status.info,
iconBackgroundColor = TangemTheme.colors3.bg.status.infoSubtle,
primaryButtonTextRes = R.string.tangempay_issue_card,
primaryAction = { it.onIssueClick },
)
}
private fun IssueButton(state: TangemPayIssueAdditionalCardUM, modifier: Modifier = Modifier) {
TangemButton(
modifier = modifier
.fillMaxWidth()
.padding(16.dp),
size = TangemButton.Size.X12,
onClick = state.onIssueClick,
isEnabled = !state.isLoading && !state.isBalanceInsufficient,
isLoading = state.isLoading,
text = resourceReference(R.string.tangempay_issue_card),
)
}
@Preview(showBackground = true, widthDp = 360)
@ -223,29 +228,7 @@ private fun TangemPayIssueAdditionalCardContentPreview(
@PreviewParameter(IssueAdditionalCardPreviewProvider::class) state: TangemPayIssueAdditionalCardUM,
) {
TangemThemePreviewRedesign {
IssueAdditionalCardSheetPreview(state = state)
}
}
@Composable
private fun IssueAdditionalCardSheetPreview(state: TangemPayIssueAdditionalCardUM) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors3.bg.secondary),
) {
TangemTopBar(
type = TangemTopBarType.BottomSheet,
endContent = {
TangemButton(
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24),
onClick = state.onDismiss,
size = TangemButton.Size.X11,
variant = TangemButton.Variant.Material,
)
},
)
Content(state)
TangemPayIssueAdditionalCardContent(state = state)
}
}

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
@ -30,6 +31,7 @@ import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.TangemPayTestTags
@ -96,7 +98,7 @@ private fun TangemPayMainContent(
.testTag(TangemPayTestTags.MAIN_SCREEN_TILE),
) {
Image(
painter = painterResource(R.drawable.img_visa_36),
painter = getVisaIconPainter(),
contentDescription = null,
modifier = Modifier
.layoutId(TangemRowLayoutId.HEAD)
@ -145,7 +147,7 @@ private fun TangemPayStateRow(
.conditional(onClick != null && isEnabled) { clickableSingle(onClick = requireNotNull(onClick)) },
) {
Image(
painter = painterResource(R.drawable.img_visa_36),
painter = getVisaIconPainter(),
contentDescription = null,
modifier = Modifier
.layoutId(TangemRowLayoutId.HEAD)
@ -274,6 +276,12 @@ private fun TangemPayMainLoading(modifier: Modifier = Modifier) {
}
}
@Composable
private fun getVisaIconPainter(): Painter {
val resource = if (LocalRedesignEnabled.current) R.drawable.ic_visa_in_banner else R.drawable.img_visa_36
return painterResource(resource)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -2,4 +2,5 @@ package com.tangem.features.tokendetails
interface TokenDetailsFeatureToggles {
val isQuickTopUpEnabled: Boolean
val isManageFundsEnabled: Boolean
}

View file

@ -22,7 +22,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDeta
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreenLegacy
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.ChooseAddressBottomSheetComponent
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.CloreMigrationBottomSheetComponent
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.DynamicAddressesBottomSheetComponent
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.TransferBottomSheetComponent
@ -47,7 +47,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
expressTransactionsComponentFactory: ExpressTransactionsComponent.Factory,
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
private val yieldSupplyWarningComponentFactory: YieldSupplyDepositedWarningComponent.Factory,
private val addFundsComponentFactory: AddFundsComponent.Factory,
private val manageFundsComponentFactory: ManageFundsComponent.Factory,
yieldSupplyComponentFactory: YieldSupplyComponent.Factory,
private val ratingComponentFactory: RatingComponent.Factory,
) : TokenDetailsComponent, AppComponentContext by appComponentContext {
@ -178,10 +178,10 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
dynamicAddressesDelegate = model.dynamicAddressesDelegate,
onDismiss = model.bottomSheetNavigation::dismiss,
)
is TokenDetailsBottomSheetConfig.AddFunds -> addFundsComponentFactory.create(
is TokenDetailsBottomSheetConfig.AddFunds -> manageFundsComponentFactory.create(
context = childByContext(componentContext),
params = AddFundsComponent.Params(
launchMode = AddFundsComponent.LaunchMode.TokenActionsOnly(
params = ManageFundsComponent.Params(
launchMode = ManageFundsComponent.LaunchMode.TokenActionsOnly(
userWalletId = route.userWalletId,
currency = route.currency,
),

View file

@ -6,10 +6,13 @@ import com.tangem.features.tokendetails.TokenDetailsFeatureToggles
import javax.inject.Inject
internal class DefaultTokenDetailsFeatureToggles @Inject constructor(
featureTogglesManager: FeatureTogglesManager,
private val featureTogglesManager: FeatureTogglesManager,
) : TokenDetailsFeatureToggles {
override val isQuickTopUpEnabled: Boolean = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_15258_QUICK_TOP_UP_ENABLED,
)
override val isManageFundsEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1377_MANAGE_FUNDS)
}

View file

@ -22,6 +22,8 @@ interface TokenDetailsClickIntents {
fun onSwapFromClick(unavailabilityReason: ScenarioUnavailabilityReason)
fun onSwapAndSendClick(unavailabilityReason: ScenarioUnavailabilityReason)
fun onSwapToClick(unavailabilityReason: ScenarioUnavailabilityReason)
fun onBuyClick(unavailabilityReason: ScenarioUnavailabilityReason)
@ -151,6 +153,8 @@ internal class EmptyTokenDetailsClickIntents : TokenDetailsClickIntents {
override fun onSwapFromClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
override fun onSwapAndSendClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
override fun onSwapToClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ }
override fun onHideClick() { /* no op */ }

View file

@ -599,6 +599,15 @@ internal class TokenDetailsModel @Inject constructor(
}
override fun onTransferClick() {
val amount = cryptoCurrencyStatus?.value?.amount
if (amount == null || amount.signum() <= 0) {
uiMessageSender.send(
message = SnackbarMessage(
message = resourceReference(R.string.token_button_unavailability_reason_empty_balance_send),
),
)
return
}
bottomSheetNavigation.activate(TokenDetailsBottomSheetConfig.Transfer)
}
@ -832,6 +841,20 @@ internal class TokenDetailsModel @Inject constructor(
handleSwap(unavailabilityReason, AppRoute.Swap.CurrencyPosition.FROM, checkYieldSupply = true)
}
override fun onSwapAndSendClick(unavailabilityReason: ScenarioUnavailabilityReason) {
if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) {
return
}
appRouter.push(
AppRoute.SendEntryPoint(
userWalletId = userWalletId,
currency = cryptoCurrency,
shouldStartWithSwap = true,
),
)
}
override fun onSwapToClick(unavailabilityReason: ScenarioUnavailabilityReason) {
handleSwap(unavailabilityReason, AppRoute.Swap.CurrencyPosition.TO, checkYieldSupply = false)
}

View file

@ -21,6 +21,7 @@ internal sealed interface TransferUM : TangemBottomSheetConfigContent {
data class Content(
val send: Row?,
val swap: Row?,
val swapAndSend: Row?,
val sell: Row?,
) : TransferUM

View file

@ -43,6 +43,16 @@ internal class UpdateTransferTransformer(
},
)
}
val swapAndSendRow = swapAction?.let { action ->
TransferUM.Row(
isLoading = action.unavailabilityReason.isLoading,
isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None,
onClick = {
onActionDispatched()
clickIntents.onSwapAndSendClick(action.unavailabilityReason)
},
)
}
val sellRow = sellAction?.let { action ->
TransferUM.Row(
isLoading = action.unavailabilityReason.isOutdatedLoading(),
@ -55,7 +65,12 @@ internal class UpdateTransferTransformer(
}
return prevState.copy(
transferUM = TransferUM.Content(send = sendRow, swap = swapRow, sell = sellRow),
transferUM = TransferUM.Content(
send = sendRow,
swap = swapRow,
swapAndSend = swapAndSendRow,
sell = sellRow,
),
)
}

View file

@ -36,6 +36,7 @@ internal fun TransferBottomSheetContent(state: TransferUM, onCloseClick: () -> U
) {
SendActionRow(state = state)
SwapActionRow(state = state)
SwapAndSendActionRow(state = state)
SellActionRow(state = state)
SpacerH(TangemTheme.dimens2.x2)
@ -80,6 +81,19 @@ private fun SwapActionRow(state: TransferUM) {
)
}
@Composable
private fun SwapAndSendActionRow(state: TransferUM) {
val row = (state as? TransferUM.Content)?.swapAndSend
if (state is TransferUM.Content && row == null) return
ActionRow(
iconRes = CoreR.drawable.ic_exchange_mini_24,
title = resourceReference(CoreR.string.common_send_with_swap),
description = resourceReference(CoreR.string.quick_action_send_and_swap_description),
row = row,
isLoading = state is TransferUM.Loading,
)
}
@Composable
private fun SellActionRow(state: TransferUM) {
val row = (state as? TransferUM.Content)?.sell
@ -149,16 +163,19 @@ private class TransferPreviewProvider : PreviewParameterProvider<TransferUM> {
TransferUM.Content(
send = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}),
swap = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}),
swapAndSend = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}),
sell = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}),
),
TransferUM.Content(
send = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}),
swap = TransferUM.Row(isLoading = false, isEnabled = false, onClick = {}),
swapAndSend = TransferUM.Row(isLoading = false, isEnabled = false, onClick = {}),
sell = TransferUM.Row(isLoading = false, isEnabled = false, onClick = {}),
),
TransferUM.Content(
send = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}),
swap = null,
swapAndSend = null,
sell = null,
),
)

View file

@ -319,6 +319,57 @@ class UpdateTransferTransformerTest {
assertThat(content.swap?.isEnabled).isTrue()
}
@Test
fun `GIVEN Swap action available WHEN transform THEN swapAndSend row is not null`() {
// Arrange
val transformer = createTransformer(
actions = listOf(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None, false)),
)
// Act
val result = transformer.transform(initialState())
// Assert
val content = result.transferUM as TransferUM.Content
assertThat(content.swapAndSend).isNotNull()
}
@Test
fun `GIVEN Swap action available WHEN swapAndSend onClick invoked THEN onSwapAndSendClick is called`() {
// Arrange
val transformer = createTransformer(
actions = listOf(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None, false)),
)
// Act
val content = transformer.transform(initialState()).transferUM as TransferUM.Content
content.swapAndSend!!.onClick()
// Assert
verifyOrder {
onActionDispatched.invoke()
clickIntents.onSwapAndSendClick(ScenarioUnavailabilityReason.None)
}
}
@Test
fun `GIVEN no Swap action WHEN transform THEN swapAndSend row is null`() {
// Arrange
val transformer = createTransformer(
actions = listOf(
TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.None),
TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None),
),
)
// Act
val result = transformer.transform(initialState())
// Assert
val content = result.transferUM as TransferUM.Content
assertThat(content.swapAndSend).isNull()
}
@Test
fun `GIVEN row WHEN not clicked THEN no callbacks fire`() {
// GIVEN

View file

@ -1,6 +0,0 @@
package com.tangem.features.txhistory
interface TxHistoryFeatureToggles {
val isSolanaTxHistoryEnabled: Boolean
val isNewTxHistoryEnabled: Boolean
}

View file

@ -9,6 +9,12 @@ plugins {
android {
namespace = "com.tangem.features.txhistory.impl"
packaging {
resources {
merges += "paymentrequest.proto"
}
}
}
dependencies {
/* Project - API */

View file

@ -83,7 +83,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
uiStatus = uiStatus,
title = stringReference(type.name),
iconRes = tx.directionalIcon(),
subtitle = ContentSubtitle.Plain(tx.extractSubtitleByAddressType()),
subtitle = tx.extractAddressSubtitle(),
)
private fun swapContent(tx: TxInfo, uiStatus: TransactionItemUM.Content.Status): TransactionItemUM.Content =
@ -92,7 +92,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
uiStatus = uiStatus,
title = tx.statusAwareTitle(R.string.common_swapping, R.string.common_swapped),
iconRes = tx.directionalIcon(),
subtitle = ContentSubtitle.Plain(tx.extractSubtitleByAddressType()),
subtitle = tx.extractAddressSubtitle(),
)
private fun transferContent(tx: TxInfo, uiStatus: TransactionItemUM.Content.Status): TransactionItemUM.Content {
@ -112,7 +112,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
rawAddress = counterpartyAddress,
briefAddress = counterpartyAddress.toBriefAddressFormat(),
)
else -> ContentSubtitle.Plain(tx.extractSubtitleByAddressType())
else -> tx.extractAddressSubtitle()
}
return buildContent(
@ -145,7 +145,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
uiStatus = uiStatus,
title = resourceReference(R.string.yield_module_transaction_topup),
iconRes = tx.directionalIcon(),
subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)),
subtitle = tx.yieldSupplySubtitle(currency, type),
)
private fun yieldDeployContractContent(
@ -157,7 +157,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
uiStatus = uiStatus,
title = resourceReference(R.string.yield_module_transaction_deploy_contract),
iconRes = R.drawable.ic_doc_24,
subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)),
subtitle = tx.yieldSupplySubtitle(currency, type),
)
private fun yieldInitializeTokenContent(
@ -169,7 +169,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
uiStatus = uiStatus,
title = resourceReference(R.string.yield_module_transaction_initialize),
iconRes = R.drawable.ic_gear_24,
subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)),
subtitle = tx.yieldSupplySubtitle(currency, type),
)
private fun yieldReactivateTokenContent(
@ -181,7 +181,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
uiStatus = uiStatus,
title = resourceReference(R.string.yield_module_transaction_reactivate),
iconRes = R.drawable.ic_refresh_24,
subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)),
subtitle = tx.yieldSupplySubtitle(currency, type),
)
private fun yieldSendContent(
@ -197,7 +197,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
resourceReference(R.string.common_transfer)
},
iconRes = tx.directionalIcon(),
subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)),
subtitle = tx.yieldSupplySubtitle(currency, type),
hideAmount = currency is CryptoCurrency.Token && !tx.isOutgoing,
)
@ -209,7 +209,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
uiStatus = uiStatus,
title = resourceReference(R.string.transaction_history_operation),
iconRes = tx.directionalIcon(),
subtitle = ContentSubtitle.Plain(tx.extractSubtitleByAddressType()),
subtitle = tx.extractAddressSubtitle(),
)
private fun gaslessFeeContent(tx: TxInfo, uiStatus: TransactionItemUM.Content.Status): TransactionItemUM.Content =
@ -218,7 +218,7 @@ internal class TxHistoryItemToTransactionItemUMConverter(
uiStatus = uiStatus,
title = resourceReference(R.string.gasless_transaction_fee),
iconRes = tx.directionalIcon(),
subtitle = ContentSubtitle.Plain(tx.extractSubtitleByAddressType()),
subtitle = tx.extractAddressSubtitle(),
)
private fun buildContent(
@ -283,15 +283,21 @@ private fun resolveOwnSubtitle(
}
}
private fun TxInfo.yieldSupplySubtitle(currency: CryptoCurrency, type: TransactionType.YieldSupply): TextReference {
private fun TxInfo.yieldSupplySubtitle(currency: CryptoCurrency, type: TransactionType.YieldSupply): ContentSubtitle {
if (currency is CryptoCurrency.Coin) {
return if (type is TransactionType.YieldSupply.Send) {
extractSubtitleByAddressType()
extractAddressSubtitle()
} else {
resourceReference(
val briefAddress = type.address?.toBriefAddressFormat()
val text = resourceReference(
R.string.transaction_history_transaction_for_address,
wrappedList(type.address?.toBriefAddressFormat().orEmpty()),
wrappedList(briefAddress.orEmpty()),
)
if (briefAddress.isNullOrEmpty()) {
ContentSubtitle.Plain(text)
} else {
ContentSubtitle.PlainAddress(text = text, highlight = briefAddress)
}
}
}
return when (type) {
@ -304,15 +310,34 @@ private fun TxInfo.yieldSupplySubtitle(currency: CryptoCurrency, type: Transacti
is TransactionType.YieldSupply.Send -> if (!isOutgoing && type.isYieldSupplyWithdraw) {
amountSubtitle(currency, R.string.yield_module_transaction_exit_subtitle)
} else {
extractSubtitleByAddressType()
extractAddressSubtitle()
}
else -> extractSubtitleByAddressType()
else -> extractAddressSubtitle()
}
}
private fun TxInfo.amountSubtitle(currency: CryptoCurrency, @StringRes resId: Int): TextReference {
private fun TxInfo.amountSubtitle(currency: CryptoCurrency, @StringRes resId: Int): ContentSubtitle.Plain {
val formatted = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
return resourceReference(resId, wrappedList(formatted))
return ContentSubtitle.Plain(resourceReference(resId, wrappedList(formatted)))
}
private fun TxInfo.extractAddressSubtitle(): ContentSubtitle {
val text = extractSubtitleByAddressType()
val highlight = interactionAddressType.highlightableBriefAddress()
return if (highlight == null) {
ContentSubtitle.Plain(text)
} else {
ContentSubtitle.PlainAddress(text = text, highlight = highlight)
}
}
private fun TxInfo.InteractionAddressType?.highlightableBriefAddress(): String? = when (this) {
is TxInfo.InteractionAddressType.Contract -> address.toBriefAddressFormat()
is TxInfo.InteractionAddressType.User -> address.toBriefAddressFormat()
is TxInfo.InteractionAddressType.Validator -> address.toBriefAddressFormat()
is TxInfo.InteractionAddressType.Multiple,
null,
-> null
}
private fun TxInfo.extractSubtitleByAddressType(): TextReference =

View file

@ -2,7 +2,7 @@ package com.tangem.features.txhistory.di
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.txhistory.TxHistoryFeatureToggles
import com.tangem.domain.txhistory.TxHistoryFeatureToggles
import javax.inject.Inject
internal class DefaultTxHistoryFeatureToggles @Inject constructor(

View file

@ -1,6 +1,6 @@
package com.tangem.features.txhistory.di
import com.tangem.features.txhistory.TxHistoryFeatureToggles
import com.tangem.domain.txhistory.TxHistoryFeatureToggles
import com.tangem.features.txhistory.component.DefaultTxHistoryComponent
import com.tangem.features.txhistory.component.DefaultTxHistoryDetailsComponent
import com.tangem.features.txhistory.component.TxHistoryComponent

View file

@ -26,7 +26,7 @@ import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.wallets.usecase.GetWalletIconUseCase
import com.tangem.features.txhistory.TxHistoryFeatureToggles
import com.tangem.domain.txhistory.TxHistoryFeatureToggles
import com.tangem.features.txhistory.component.TxHistoryComponent
import com.tangem.features.txhistory.converter.TxHistoryInfoToTransactionItemUMConverter
import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionItemUMConverter

View file

@ -341,7 +341,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
}
@Test
fun `GIVEN Transfer with non-User interaction WHEN convert THEN Plain subtitle`() {
fun `GIVEN Transfer with non-User interaction WHEN convert THEN PlainAddress subtitle`() {
val tx = txInfo(
type = TransactionType.Transfer,
isOutgoing = true,
@ -350,7 +350,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
val result = coinConverter.convert(tx) as TransactionItemUM.Content
assertThat(result.subtitle).isInstanceOf(ContentSubtitle.Plain::class.java)
assertThat(result.subtitle).isInstanceOf(ContentSubtitle.PlainAddress::class.java)
assertThat(result.title).isEqualTo(resRef(R.string.common_sent))
}
@ -575,7 +575,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
val result = coinConverter.convert(tx) as TransactionItemUM.Content
val subtitle = result.subtitle as ContentSubtitle.Plain
val subtitle = result.subtitle as ContentSubtitle.PlainAddress
val res = subtitle.text as TextReference.Res
assertThat(res.id).isEqualTo(R.string.transaction_history_contract_address)
}
@ -623,7 +623,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest {
val result = coinConverter.convert(tx) as TransactionItemUM.Content
val subtitle = result.subtitle as ContentSubtitle.Plain
val subtitle = result.subtitle as ContentSubtitle.PlainAddress
val res = subtitle.text as TextReference.Res
assertThat(res.id).isEqualTo(R.string.transaction_history_transaction_validator)
}

View file

@ -10,4 +10,6 @@ interface WalletFeatureToggles {
val isAddAndManageTokensEnabled: Boolean
val isAddFundsStage1Enabled: Boolean
val isManageFundsEnabled: Boolean
}

View file

@ -25,7 +25,7 @@ import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.tokens.model.details.TokenAction
import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent
import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent
import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent
import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent
import com.tangem.feature.wallet.child.tokenActions.DefaultTokenActionsComponent
import com.tangem.feature.wallet.child.tokenActions.TokenActionsComponent
import com.tangem.feature.wallet.child.wallet.model.WalletModel
@ -70,7 +70,7 @@ internal class WalletComponent @AssistedInject constructor(
private val networkSelectionComponentFactory: NetworkSelectionComponent.Factory,
private val tokenActionsComponentFactory: TokenActionsComponent.Factory,
private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
private val addFundsComponentFactory: AddFundsComponent.Factory,
private val manageFundsComponentFactory: ManageFundsComponent.Factory,
private val designFeatureToggles: DesignFeatureToggles,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
@ -191,10 +191,20 @@ internal class WalletComponent @AssistedInject constructor(
)
}
is WalletDialogConfig.AddFunds -> {
addFundsComponentFactory.create(
manageFundsComponentFactory.create(
context = childByContext(componentContext),
params = AddFundsComponent.Params(
launchMode = AddFundsComponent.LaunchMode.ChooseToken(dialogConfig.userWalletId),
params = ManageFundsComponent.Params(
launchMode = ManageFundsComponent.LaunchMode.ChooseToken(dialogConfig.userWalletId),
onDismiss = model.innerWalletRouter.dialogNavigation::dismiss,
),
)
}
is WalletDialogConfig.Transfer -> {
manageFundsComponentFactory.create(
context = childByContext(componentContext),
params = ManageFundsComponent.Params(
launchMode = ManageFundsComponent.LaunchMode.ChooseToken(dialogConfig.userWalletId),
flowType = ManageFundsComponent.FlowType.Transfer,
onDismiss = model.innerWalletRouter.dialogNavigation::dismiss,
),
)

View file

@ -566,6 +566,7 @@ internal class WalletModel @Inject constructor(
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled,
isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled,
),
)
@ -613,6 +614,7 @@ internal class WalletModel @Inject constructor(
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled,
isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled,
),
)
}
@ -635,6 +637,7 @@ internal class WalletModel @Inject constructor(
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled,
isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled,
),
)
}
@ -650,6 +653,7 @@ internal class WalletModel @Inject constructor(
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled,
isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled,
),
)
@ -712,6 +716,7 @@ internal class WalletModel @Inject constructor(
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled,
isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled,
),
)

View file

@ -124,6 +124,10 @@ internal class WalletClickIntents @Inject constructor(
router.openAddFunds(userWalletId)
}
fun onTransferClick(userWalletId: UserWalletId) {
router.openTransfer(userWalletId)
}
fun onAddFundsPromoClick(userWalletId: UserWalletId) {
analyticsEventHandler.send(WalletScreenAnalyticsEvent.MainScreen.ButtonAddFundsPromo())
router.openAddFunds(userWalletId)

View file

@ -14,4 +14,7 @@ internal class DefaultWalletFeatureToggles @Inject constructor(
override val isAddFundsStage1Enabled: Boolean
get() = featureToggles.isFeatureEnabled(FeatureToggles.AND_15310_ADD_FUNDS_STAGE1)
override val isManageFundsEnabled: Boolean
get() = featureToggles.isFeatureEnabled(FeatureToggles.TWI_1377_MANAGE_FUNDS)
}

View file

@ -125,6 +125,12 @@ internal class DefaultWalletRouter @Inject constructor(
)
}
override fun openTransfer(userWalletId: UserWalletId) {
dialogNavigation.activate(
configuration = WalletDialogConfig.Transfer(userWalletId = userWalletId),
)
}
override fun isWalletLastScreen(): Boolean {
return router.stack.lastOrNull() is AppRoute.Wallet
}

View file

@ -120,4 +120,7 @@ internal interface InnerWalletRouter {
/** Open Add Funds screen */
fun openAddFunds(userWalletId: UserWalletId)
/** Open Transfer screen */
fun openTransfer(userWalletId: UserWalletId)
}

View file

@ -56,7 +56,7 @@ internal sealed class WalletActionButtons(
override val isEnabled: Boolean,
) : WalletActionButtons(
text = resourceReference(R.string.common_add_funds),
iconRes = R.drawable.ic_plus_default_24,
iconRes = R.drawable.ic_arrow_down_24,
)
data class Swap(
@ -74,4 +74,12 @@ internal sealed class WalletActionButtons(
text = resourceReference(R.string.common_sell),
iconRes = R.drawable.ic_dollar_default_24,
)
data class Transfer(
override val onClick: () -> Unit,
override val isEnabled: Boolean,
) : WalletActionButtons(
text = resourceReference(R.string.common_transfer),
iconRes = R.drawable.ic_arrow_up_24,
)
}

View file

@ -65,6 +65,9 @@ internal sealed interface WalletDialogConfig {
@Serializable
data class AddFunds(val userWalletId: UserWalletId) : WalletDialogConfig
@Serializable
data class Transfer(val userWalletId: UserWalletId) : WalletDialogConfig
@Serializable
data class OrganizeTokens(val userWalletId: UserWalletId) : WalletDialogConfig

View file

@ -58,7 +58,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
) : WalletManageButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_add_funds),
iconResId = R.drawable.ic_plus_24,
iconResId = R.drawable.ic_arrow_down_24,
onClick = onClick,
isEnabled = enabled,
shouldDimContent = dimContent,
@ -145,6 +145,29 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
),
)
/**
* Transfer
*
* @property enabled button click availability
* @property dimContent determines whether the button content will be dimmed
* @property onClick lambda be invoked when Transfer button is clicked
*/
data class Transfer(
override val enabled: Boolean,
override val dimContent: Boolean,
override val onClick: () -> Unit,
val isInProgress: Boolean = false,
) : WalletManageButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_transfer),
iconResId = R.drawable.ic_arrow_up_24,
onClick = onClick,
isEnabled = enabled,
shouldDimContent = dimContent,
isInProgress = isInProgress,
),
)
/**
* Swap
*

View file

@ -328,8 +328,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
isLoading = shouldShowProgress,
),
),
messageEffect = TangemMessageEffect.Card,
isCentered = true,
messageEffect = TangemMessageEffect.Warning,
),
type = WalletNotificationType.Warning,
)
@ -339,9 +338,10 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
id = "TangemPayUnreachable",
title = resourceReference(id = R.string.tangempay_temporarily_unavailable),
subtitle = resourceReference(id = R.string.tangempay_service_unreachable_try_later),
messageEffect = TangemMessageEffect.Warning,
iconUM = TangemIconUM.Icon(
iconRes = R.drawable.ic_attention_default_24,
tintReference = { TangemTheme.colors2.graphic.status.attention },
iconRes = R.drawable.ic_alert_circle_24,
tintReference = { TangemTheme.colors2.graphic.neutral.primary },
),
),
type = WalletNotificationType.Warning,

View file

@ -14,6 +14,7 @@ internal class AddWalletTransformer(
private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val isAddFundsStage1Enabled: Boolean,
private val isManageFundsEnabled: Boolean,
) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy {
@ -22,6 +23,7 @@ internal class AddWalletTransformer(
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = isAddFundsStage1Enabled,
isManageFundsEnabled = isManageFundsEnabled,
)
}

View file

@ -21,6 +21,7 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import com.tangem.core.ui.R as CoreUiR
@Suppress("LongParameterList")
internal class InitializeWalletsTransformer(
private val selectedWalletIndex: Int,
private val wallets: List<UserWallet>,
@ -28,6 +29,7 @@ internal class InitializeWalletsTransformer(
private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val isAddFundsStage1Enabled: Boolean,
private val isManageFundsEnabled: Boolean,
) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy {
@ -36,6 +38,7 @@ internal class InitializeWalletsTransformer(
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = isAddFundsStage1Enabled,
isManageFundsEnabled = isManageFundsEnabled,
)
}
@ -159,10 +162,16 @@ internal class InitializeWalletsTransformer(
WalletManageButton.Buy(enabled = false, dimContent = false, onClick = {})
}
val lastButton = if (isManageFundsEnabled) {
WalletManageButton.Transfer(enabled = false, dimContent = false, onClick = {})
} else {
WalletManageButton.Sell(enabled = false, dimContent = false, onClick = {})
}
return persistentListOf(
firstButton,
WalletManageButton.Swap(enabled = false, dimContent = false, onClick = {}),
WalletManageButton.Sell(enabled = false, dimContent = false, onClick = {}),
lastButton,
)
}
@ -190,12 +199,21 @@ internal class InitializeWalletsTransformer(
onClick = {},
).buttonUM,
)
add(
WalletActionButtons.Sell(
isEnabled = false,
onClick = {},
).buttonUM,
)
if (isManageFundsEnabled) {
add(
WalletActionButtons.Transfer(
isEnabled = false,
onClick = {},
).buttonUM,
)
} else {
add(
WalletActionButtons.Sell(
isEnabled = false,
onClick = {},
).buttonUM,
)
}
}.toPersistentList()
}
}

View file

@ -18,6 +18,7 @@ import kotlinx.collections.immutable.toImmutableList
*
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
internal class ReinitializeNewWalletTransformer(
private val prevWalletId: UserWalletId,
private val newUserWallet: UserWallet,
@ -25,6 +26,7 @@ internal class ReinitializeNewWalletTransformer(
private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val isAddFundsStage1Enabled: Boolean,
private val isManageFundsEnabled: Boolean,
) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy {
@ -33,6 +35,7 @@ internal class ReinitializeNewWalletTransformer(
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = isAddFundsStage1Enabled,
isManageFundsEnabled = isManageFundsEnabled,
)
}

View file

@ -21,6 +21,7 @@ internal class ReinitializeWalletTransformer(
private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val isAddFundsStage1Enabled: Boolean,
private val isManageFundsEnabled: Boolean,
) : WalletStateTransformer(userWalletId = userWallet.walletId) {
private val walletLoadingStateFactory by lazy {
@ -29,6 +30,7 @@ internal class ReinitializeWalletTransformer(
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = isAddFundsStage1Enabled,
isManageFundsEnabled = isManageFundsEnabled,
)
}

View file

@ -87,6 +87,7 @@ internal class SetRefreshStateTransformer(
is WalletManageButton.AddFunds -> button.copy(enabled = isButtonsEnabled)
is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled)
is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled)
is WalletManageButton.Transfer -> button.copy(enabled = isButtonsEnabled)
is WalletManageButton.Receive -> button
is WalletManageButton.Stake -> null
is WalletManageButton.Swap -> null

View file

@ -19,6 +19,7 @@ internal class UnlockWalletTransformer(
private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val isAddFundsStage1Enabled: Boolean,
private val isManageFundsEnabled: Boolean,
) : WalletScreenStateTransformer {
private val walletLoadingStateFactory by lazy {
@ -27,6 +28,7 @@ internal class UnlockWalletTransformer(
walletImageResolver = walletImageResolver,
getWalletIconUseCase = getWalletIconUseCase,
isAddFundsStage1Enabled = isAddFundsStage1Enabled,
isManageFundsEnabled = isManageFundsEnabled,
)
}

View file

@ -45,6 +45,7 @@ private fun WalletState.MultiCurrency.Content.changeAvailability(enabled: Boolea
is WalletManageButton.Buy -> action.copy(enabled = enabled)
is WalletManageButton.AddFunds -> action.copy(enabled = enabled)
is WalletManageButton.Sell -> action.copy(enabled = enabled)
is WalletManageButton.Transfer -> action.copy(enabled = enabled)
is WalletManageButton.Swap -> action.copy(enabled = enabled)
else -> action
}

View file

@ -36,6 +36,7 @@ internal class WalletLoadingStateFactory(
private val walletImageResolver: WalletImageResolver,
private val getWalletIconUseCase: GetWalletIconUseCase,
private val isAddFundsStage1Enabled: Boolean,
private val isManageFundsEnabled: Boolean,
) {
fun create(userWallet: UserWallet): WalletState {
@ -171,6 +172,20 @@ internal class WalletLoadingStateFactory(
)
}
val lastButton = if (isManageFundsEnabled) {
WalletManageButton.Transfer(
enabled = true,
dimContent = false,
onClick = { clickIntents.onTransferClick(userWallet.walletId) },
)
} else {
WalletManageButton.Sell(
enabled = true,
dimContent = false,
onClick = { clickIntents.onMultiWalletSellClick(userWalletId = userWallet.walletId) },
)
}
return persistentListOf(
firstButton,
WalletManageButton.Swap(
@ -178,11 +193,7 @@ internal class WalletLoadingStateFactory(
dimContent = false,
onClick = { clickIntents.onMultiWalletSwapClick(userWalletId = userWallet.walletId) },
),
WalletManageButton.Sell(
enabled = true,
dimContent = false,
onClick = { clickIntents.onMultiWalletSellClick(userWalletId = userWallet.walletId) },
),
lastButton,
)
}
@ -205,14 +216,25 @@ internal class WalletLoadingStateFactory(
},
).buttonUM,
)
add(
WalletActionButtons.Sell(
isEnabled = false,
onClick = {
clickIntents.onMultiWalletSellClick(userWalletId = userWallet.walletId)
},
).buttonUM,
)
if (isManageFundsEnabled) {
add(
WalletActionButtons.Transfer(
isEnabled = false,
onClick = {
clickIntents.onTransferClick(userWalletId = userWallet.walletId)
},
).buttonUM,
)
} else {
add(
WalletActionButtons.Sell(
isEnabled = false,
onClick = {
clickIntents.onMultiWalletSellClick(userWalletId = userWallet.walletId)
},
).buttonUM,
)
}
}.toPersistentList()
}