From b1dcdb4d13d7775fdae621182ec9e42d69a7aeed Mon Sep 17 00:00:00 2001 From: John Huang Date: Thu, 9 May 2024 11:59:23 -0700 Subject: [PATCH] Features/mob 400 adjust margin screen bottom (#87) * Margin type and leverage screens skeleton * MOB-356 MOB-358 Margin mode screen * Change bg color * MOB-360 rough UX for target leverage screen * Fixed PR * move modifier to param * In the middle of coding * lint * There is no longer InputFieldScarfold * Put back InputFieldScaffold * More placeholder code and it compiles * rough amount input * Formatting "Add Margin" and "Remove Margin" * MOB-400 placeholder for bottom * Adjust margin receipt area * MOB-400 rearranging receipt data * MOB-400 liquidation price * Fix copy and modified code * PR review * spotless * fix build --- .../feature/receipt/DydxReceiptView.kt | 25 +- .../DydxReceiptFreeCollateralView.kt | 117 +++++++ .../DydxReceiptFreeCollateralViewModel.kt | 120 +++++++ ...DydxReceiptIsolatedPositionLeverageView.kt | 107 +++++++ ...eceiptIsolatedPositionLeverageViewModel.kt | 65 ++++ ...xReceiptIsolatedPositionMarginUsageView.kt | 109 +++++++ ...iptIsolatedPositionMarginUsageViewModel.kt | 62 ++++ .../DydxReceiptLiquidationPriceView.kt | 104 ++++++ .../DydxReceiptLiquidationPriceViewModel.kt | 120 +++++++ .../feature/shared/views/AmountText.kt | 2 +- .../feature/shared/views/LeverageView.kt | 97 ++++++ .../trade/margin/DydxAdjustMarginInputView.kt | 298 +++++++++++++----- .../margin/DydxAdjustMarginInputViewModel.kt | 93 +++++- .../components/DydxAdjustMarginCtaButton.kt | 50 +++ .../DydxAdjustMarginCtaButtonModel.kt | 54 ++++ 15 files changed, 1334 insertions(+), 89 deletions(-) create mode 100644 v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/freecollateral/DydxReceiptFreeCollateralView.kt create mode 100644 v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/freecollateral/DydxReceiptFreeCollateralViewModel.kt create mode 100644 v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionLeverageView.kt create mode 100644 v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionLeverageViewModel.kt create mode 100644 v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionMarginUsageView.kt create mode 100644 v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionMarginUsageViewModel.kt create mode 100644 v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/liquidationprice/DydxReceiptLiquidationPriceView.kt create mode 100644 v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/liquidationprice/DydxReceiptLiquidationPriceViewModel.kt create mode 100644 v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/views/LeverageView.kt create mode 100644 v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/components/DydxAdjustMarginCtaButton.kt create mode 100644 v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/components/DydxAdjustMarginCtaButtonModel.kt diff --git a/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/DydxReceiptView.kt b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/DydxReceiptView.kt index a1fe7080..422342d3 100644 --- a/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/DydxReceiptView.kt +++ b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/DydxReceiptView.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import exchange.dydx.abacus.protocols.LocalizerProtocol @@ -24,6 +25,7 @@ import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer import exchange.dydx.trading.feature.receipt.components.buyingpower.DydxReceiptBuyingPowerView +import exchange.dydx.trading.feature.receipt.components.buyingpower.DydxReceiptFreeCollateralView import exchange.dydx.trading.feature.receipt.components.equity.DydxReceiptEquityView import exchange.dydx.trading.feature.receipt.components.exchangerate.DydxReceiptExchangeRateView import exchange.dydx.trading.feature.receipt.components.exchangereceived.DydxReceiptExchangeReceivedView @@ -31,6 +33,8 @@ import exchange.dydx.trading.feature.receipt.components.expectedprice.DydxReceip import exchange.dydx.trading.feature.receipt.components.fee.DydxReceiptBridgeFeeView import exchange.dydx.trading.feature.receipt.components.fee.DydxReceiptFeeView import exchange.dydx.trading.feature.receipt.components.fee.DydxReceiptGasFeeView +import exchange.dydx.trading.feature.receipt.components.isolatedmargin.DydxReceiptIsolatedPositionLeverageView +import exchange.dydx.trading.feature.receipt.components.isolatedmargin.DydxReceiptIsolatedPositionMarginUsageView import exchange.dydx.trading.feature.receipt.components.marginusage.DydxReceiptMarginUsageView import exchange.dydx.trading.feature.receipt.components.rewards.DydxReceiptRewardsView import exchange.dydx.trading.feature.receipt.components.slippage.DydxReceiptSlippageView @@ -46,8 +50,11 @@ fun Preview_DydxReceiptView() { object DydxReceiptView : DydxComponent { enum class ReceiptLineType { + FreeCollateral, BuyingPower, MarginUsage, + IsolatedPositionLeverage, + IsolatedPositionMarginUsage, Fee, GasFee, BridgeFee, @@ -62,6 +69,8 @@ object DydxReceiptView : DydxComponent { data class ViewState( val localizer: LocalizerProtocol, + val height: Dp? = null, + val padding: Dp? = null, val lineTypes: List = emptyList(), ) { companion object { @@ -93,9 +102,9 @@ object DydxReceiptView : DydxComponent { Box( modifier = modifier - .height(210.dp) + .height(state.height ?: 210.dp) .fillMaxWidth() - .padding(horizontal = ThemeShapes.HorizontalPadding) + .padding(horizontal = state.padding ?: ThemeShapes.HorizontalPadding) .background( color = ThemeColor.SemanticColor.layer_1.color, shape = RoundedCornerShape(10.dp), @@ -110,6 +119,10 @@ object DydxReceiptView : DydxComponent { ) { items(state.lineTypes, key = { it }) { lineType -> when (lineType) { + ReceiptLineType.FreeCollateral -> { + DydxReceiptFreeCollateralView.Content(Modifier.animateItemPlacement()) + } + ReceiptLineType.BuyingPower -> { DydxReceiptBuyingPowerView.Content(Modifier.animateItemPlacement()) } @@ -118,6 +131,14 @@ object DydxReceiptView : DydxComponent { DydxReceiptMarginUsageView.Content(Modifier.animateItemPlacement()) } + ReceiptLineType.IsolatedPositionLeverage -> { + DydxReceiptIsolatedPositionLeverageView.Content(Modifier.animateItemPlacement()) + } + + ReceiptLineType.IsolatedPositionMarginUsage -> { + DydxReceiptIsolatedPositionMarginUsageView.Content(Modifier.animateItemPlacement()) + } + ReceiptLineType.Fee -> { DydxReceiptFeeView.Content(Modifier.animateItemPlacement()) } diff --git a/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/freecollateral/DydxReceiptFreeCollateralView.kt b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/freecollateral/DydxReceiptFreeCollateralView.kt new file mode 100644 index 00000000..3a13d22b --- /dev/null +++ b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/freecollateral/DydxReceiptFreeCollateralView.kt @@ -0,0 +1,117 @@ +package exchange.dydx.trading.feature.receipt.components.buyingpower + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.components.changes.PlatformAmountChange +import exchange.dydx.platformui.components.changes.PlatformDirection +import exchange.dydx.platformui.designSystem.theme.ThemeColor +import exchange.dydx.platformui.designSystem.theme.ThemeFont +import exchange.dydx.platformui.designSystem.theme.dydxDefault +import exchange.dydx.platformui.designSystem.theme.themeColor +import exchange.dydx.platformui.designSystem.theme.themeFont +import exchange.dydx.trading.common.component.DydxComponent +import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle +import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface +import exchange.dydx.trading.common.theme.MockLocalizer +import exchange.dydx.trading.feature.shared.views.AmountText + +@Preview +@Composable +fun Preview_DydxReceiptFreeCollateralView() { + DydxThemedPreviewSurface { + DydxReceiptFreeCollateralView.Content( + Modifier, + DydxReceiptFreeCollateralView.ViewState.preview, + ) + } +} + +object DydxReceiptFreeCollateralView : DydxComponent { + data class ViewState( + val localizer: LocalizerProtocol, + val before: AmountText.ViewState? = null, + val after: AmountText.ViewState? = null, + + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + before = AmountText.ViewState.preview, + after = AmountText.ViewState.preview, + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val viewModel: DydxReceiptFreeCollateralViewModel = hiltViewModel() + + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) return + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = state.localizer.localize("APP.GENERAL.FREE_COLLATERAL"), + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + + Spacer(modifier = Modifier.weight(0.1f)) + + PlatformAmountChange( + modifier = Modifier.weight(1f), + before = if (state.before != null) { + { + AmountText.Content( + state = state.before, + textStyle = TextStyle.dydxDefault + .themeFont( + fontType = ThemeFont.FontType.number, + fontSize = ThemeFont.FontSize.small, + ) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } + } else { + null + }, + after = if (state.after != null) { + { + AmountText.Content( + state = state.after, + textStyle = TextStyle.dydxDefault + .themeFont( + fontType = ThemeFont.FontType.number, + fontSize = ThemeFont.FontSize.small, + ) + .themeColor(ThemeColor.SemanticColor.text_primary), + ) + } + } else { + null + }, + direction = PlatformDirection.from(state.before?.amount, state.after?.amount), + textStyle = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } + } +} diff --git a/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/freecollateral/DydxReceiptFreeCollateralViewModel.kt b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/freecollateral/DydxReceiptFreeCollateralViewModel.kt new file mode 100644 index 00000000..4898face --- /dev/null +++ b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/freecollateral/DydxReceiptFreeCollateralViewModel.kt @@ -0,0 +1,120 @@ +package exchange.dydx.trading.feature.receipt.components.buyingpower + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.Subaccount +import exchange.dydx.abacus.output.SubaccountPosition +import exchange.dydx.abacus.output.input.TradeInput +import exchange.dydx.abacus.output.input.TransferInput +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.formatter.DydxFormatter +import exchange.dydx.trading.feature.receipt.ReceiptType +import exchange.dydx.trading.feature.shared.views.AmountText +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +@HiltViewModel +class DydxReceiptFreeCollateralViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, + private val receiptTypeFlow: Flow<@JvmSuppressWildcards ReceiptType?>, +) : ViewModel(), DydxViewModel { + + @OptIn(ExperimentalCoroutinesApi::class) + val state: Flow = + receiptTypeFlow + .flatMapLatest { receiptType -> + when (receiptType) { + is ReceiptType.Trade -> { + combine( + abacusStateManager.state.selectedSubaccountPositions, + abacusStateManager.state.tradeInput, + ) { positions, tradeInput -> + createViewState(positions, tradeInput) + } + } + is ReceiptType.Transfer -> { + combine( + abacusStateManager.state.selectedSubaccount, + abacusStateManager.state.transferInput, + ) { subaccount, transferInput -> + createViewState(subaccount, transferInput) + } + } + else -> flowOf() + } + } + .distinctUntilChanged() + + private fun createViewState( + positions: List?, + tradeInput: TradeInput?, + ): DydxReceiptFreeCollateralView.ViewState { + val marketId = tradeInput?.marketId ?: "ETH-USD" + val position = positions?.firstOrNull { it.id == marketId } + return DydxReceiptFreeCollateralView.ViewState( + localizer = localizer, + before = if (position?.freeCollateral?.current != null) { + AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = position.freeCollateral?.current, + tickSize = 0, + requiresPositive = true, + ) + } else { + null + }, + after = if (position?.freeCollateral?.postOrder != null) { + AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = position.freeCollateral?.postOrder, + tickSize = 0, + requiresPositive = true, + ) + } else { + null + }, + ) + } + + private fun createViewState( + subaccount: Subaccount?, + transferInput: TransferInput?, + ): DydxReceiptFreeCollateralView.ViewState { + return DydxReceiptFreeCollateralView.ViewState( + localizer = localizer, + before = if (subaccount?.freeCollateral?.current != null) { + AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = subaccount.freeCollateral?.current, + tickSize = 0, + requiresPositive = true, + ) + } else { + null + }, + after = if (subaccount?.freeCollateral?.postOrder != null) { + AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = subaccount.freeCollateral?.postOrder, + tickSize = 0, + requiresPositive = true, + ) + } else { + null + }, + ) + } +} diff --git a/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionLeverageView.kt b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionLeverageView.kt new file mode 100644 index 00000000..7a3ee865 --- /dev/null +++ b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionLeverageView.kt @@ -0,0 +1,107 @@ +package exchange.dydx.trading.feature.receipt.components.isolatedmargin + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.components.changes.PlatformAmountChange +import exchange.dydx.platformui.components.changes.PlatformDirection +import exchange.dydx.platformui.designSystem.theme.ThemeColor +import exchange.dydx.platformui.designSystem.theme.ThemeFont +import exchange.dydx.platformui.designSystem.theme.dydxDefault +import exchange.dydx.platformui.designSystem.theme.themeColor +import exchange.dydx.platformui.designSystem.theme.themeFont +import exchange.dydx.trading.common.component.DydxComponent +import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle +import exchange.dydx.trading.common.formatter.DydxFormatter +import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface +import exchange.dydx.trading.common.theme.MockLocalizer +import exchange.dydx.trading.feature.shared.views.LeverageView + +@Preview +@Composable +fun Preview_DydxReceiptLeverageView() { + DydxThemedPreviewSurface { + DydxReceiptIsolatedPositionLeverageView.Content(Modifier, DydxReceiptIsolatedPositionLeverageView.ViewState.preview) + } +} + +object DydxReceiptIsolatedPositionLeverageView : DydxComponent { + data class ViewState( + val localizer: LocalizerProtocol, + val formatter: DydxFormatter, + val before: LeverageView.ViewState? = null, + val after: LeverageView.ViewState? = null, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + formatter = DydxFormatter(), + before = LeverageView.ViewState.preview, + after = LeverageView.ViewState.preview, + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val viewModel: DydxReceiptIsolatedPositionLeverageViewModel = hiltViewModel() + + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) return + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = state.localizer.localize("APP.TRADE.POSITION_LEVERAGE"), + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + + Spacer(modifier = Modifier.weight(0.1f)) + + PlatformAmountChange( + modifier = Modifier.weight(1f), + before = if (state.before != null) { { + LeverageView.Content( + state = state.before, + textStyle = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small, fontType = ThemeFont.FontType.number) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } } else { + null + }, + after = + if (state.after != null) { { + LeverageView.Content( + state = state.after, + textStyle = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small, fontType = ThemeFont.FontType.number) + .themeColor(ThemeColor.SemanticColor.text_primary), + ) + } } else { + null + }, + direction = PlatformDirection.from(state.after?.leverage, state.before?.leverage), + textStyle = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } + } +} diff --git a/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionLeverageViewModel.kt b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionLeverageViewModel.kt new file mode 100644 index 00000000..c892e35c --- /dev/null +++ b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionLeverageViewModel.kt @@ -0,0 +1,65 @@ +package exchange.dydx.trading.feature.receipt.components.isolatedmargin + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.TradeStatesWithDoubleValues +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.formatter.DydxFormatter +import exchange.dydx.trading.feature.shared.views.LeverageView +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class DydxReceiptIsolatedPositionLeverageViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, +) : ViewModel(), DydxViewModel { + + val state: Flow = + abacusStateManager.marketId + .map { + createViewState(it) + } + .distinctUntilChanged() + + private fun createViewState( + marketId: String? + ): DydxReceiptIsolatedPositionLeverageView.ViewState { + val position = if (marketId != null) abacusStateManager.state.selectedSubaccountPositionOfMarket(marketId).value else null + val leverage: TradeStatesWithDoubleValues? = position?.leverage + val margin: TradeStatesWithDoubleValues? = null // position?.margin + /* + TODO: After abacus exposes Leverage, changes to next line + if (marketId != null) abacusStateManager.state.selectedSubaccountPositionOfMarket(marketId).value?.leverage else null + */ + return DydxReceiptIsolatedPositionLeverageView.ViewState( + localizer = localizer, + formatter = formatter, + before = if (leverage?.current != null) { + LeverageView.ViewState( + localizer = localizer, + formatter = formatter, + leverage = leverage.current ?: 0.0, + margin = margin?.current, + ) + } else { + null + }, + after = if (leverage?.postOrder != null) { + LeverageView.ViewState( + localizer = localizer, + formatter = formatter, + leverage = leverage.postOrder ?: 0.0, + margin = margin?.postOrder, + ) + } else { + null + }, + ) + } +} diff --git a/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionMarginUsageView.kt b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionMarginUsageView.kt new file mode 100644 index 00000000..0eabf582 --- /dev/null +++ b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionMarginUsageView.kt @@ -0,0 +1,109 @@ +package exchange.dydx.trading.feature.receipt.components.isolatedmargin + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.components.changes.PlatformAmountChange +import exchange.dydx.platformui.components.changes.PlatformDirection +import exchange.dydx.platformui.designSystem.theme.ThemeColor +import exchange.dydx.platformui.designSystem.theme.ThemeFont +import exchange.dydx.platformui.designSystem.theme.dydxDefault +import exchange.dydx.platformui.designSystem.theme.themeColor +import exchange.dydx.platformui.designSystem.theme.themeFont +import exchange.dydx.trading.common.component.DydxComponent +import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle +import exchange.dydx.trading.common.formatter.DydxFormatter +import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface +import exchange.dydx.trading.common.theme.MockLocalizer +import exchange.dydx.trading.feature.shared.views.MarginUsageView + +@Preview +@Composable +fun Preview_DydxReceiptMarginUsageView() { + DydxThemedPreviewSurface { + DydxReceiptIsolatedPositionMarginUsageView.Content(Modifier, DydxReceiptIsolatedPositionMarginUsageView.ViewState.preview) + } +} + +object DydxReceiptIsolatedPositionMarginUsageView : DydxComponent { + data class ViewState( + val localizer: LocalizerProtocol, + val formatter: DydxFormatter, + val before: MarginUsageView.ViewState? = null, + val after: MarginUsageView.ViewState? = null, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + formatter = DydxFormatter(), + before = MarginUsageView.ViewState.preview, + after = MarginUsageView.ViewState.preview, + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val viewModel: DydxReceiptIsolatedPositionMarginUsageViewModel = hiltViewModel() + + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) return + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = state.localizer.localize("APP.TRADE.POSITION_MARGIN"), + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + + Spacer(modifier = Modifier.weight(0.1f)) + + PlatformAmountChange( + modifier = Modifier.weight(1f), + before = if (state.before != null) { { + MarginUsageView.Content( + state = state.before, + formatter = state.formatter, + textStyle = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small, fontType = ThemeFont.FontType.number) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } } else { + null + }, + after = + if (state.after != null) { { + MarginUsageView.Content( + state = state.after, + formatter = state.formatter, + textStyle = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small, fontType = ThemeFont.FontType.number) + .themeColor(ThemeColor.SemanticColor.text_primary), + ) + } } else { + null + }, + direction = PlatformDirection.from(state.after?.percent, state.before?.percent), + textStyle = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } + } +} diff --git a/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionMarginUsageViewModel.kt b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionMarginUsageViewModel.kt new file mode 100644 index 00000000..82a128ad --- /dev/null +++ b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/isolatedmargin/DydxReceiptIsolatedPositionMarginUsageViewModel.kt @@ -0,0 +1,62 @@ +package exchange.dydx.trading.feature.receipt.components.isolatedmargin + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.TradeStatesWithDoubleValues +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.formatter.DydxFormatter +import exchange.dydx.trading.feature.shared.views.MarginUsageView +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class DydxReceiptIsolatedPositionMarginUsageViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, +) : ViewModel(), DydxViewModel { + + val state: Flow = + abacusStateManager.marketId + .map { + createViewState(it) + } + .distinctUntilChanged() + + private fun createViewState( + marketId: String? + ): DydxReceiptIsolatedPositionMarginUsageView.ViewState { + val marginUsage: TradeStatesWithDoubleValues? = + null + /* + TODO: After abacus exposes marginUsage, changes to next line + if (marketId != null) abacusStateManager.state.selectedSubaccountPositionOfMarket(marketId).value?.leverage else null + */ + return DydxReceiptIsolatedPositionMarginUsageView.ViewState( + localizer = localizer, + formatter = formatter, + before = if (marginUsage?.current != null) { + MarginUsageView.ViewState( + localizer = localizer, + displayOption = MarginUsageView.DisplayOption.IconAndValue, + percent = marginUsage?.current ?: 0.0, + ) + } else { + null + }, + after = if (marginUsage?.postOrder != null) { + MarginUsageView.ViewState( + localizer = localizer, + displayOption = MarginUsageView.DisplayOption.IconAndValue, + percent = marginUsage?.postOrder ?: 0.0, + ) + } else { + null + }, + ) + } +} diff --git a/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/liquidationprice/DydxReceiptLiquidationPriceView.kt b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/liquidationprice/DydxReceiptLiquidationPriceView.kt new file mode 100644 index 00000000..996ebac1 --- /dev/null +++ b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/liquidationprice/DydxReceiptLiquidationPriceView.kt @@ -0,0 +1,104 @@ +package exchange.dydx.trading.feature.receipt.components.liquidationprice + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.components.changes.PlatformAmountChange +import exchange.dydx.platformui.components.changes.PlatformDirection +import exchange.dydx.platformui.designSystem.theme.ThemeColor +import exchange.dydx.platformui.designSystem.theme.ThemeFont +import exchange.dydx.platformui.designSystem.theme.dydxDefault +import exchange.dydx.platformui.designSystem.theme.themeColor +import exchange.dydx.platformui.designSystem.theme.themeFont +import exchange.dydx.trading.common.component.DydxComponent +import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle +import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface +import exchange.dydx.trading.common.theme.MockLocalizer +import exchange.dydx.trading.feature.shared.views.AmountText + +@Preview +@Composable +fun Preview_DydxReceiptLiquidationPriceView() { + DydxThemedPreviewSurface { + DydxReceiptLiquidationPriceView.Content(Modifier, DydxReceiptLiquidationPriceView.ViewState.preview) + } +} + +object DydxReceiptLiquidationPriceView : DydxComponent { + data class ViewState( + val localizer: LocalizerProtocol, + val before: AmountText.ViewState? = null, + val after: AmountText.ViewState? = null, + + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + before = AmountText.ViewState.preview, + after = AmountText.ViewState.preview, + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val viewModel: DydxReceiptLiquidationPriceViewModel = hiltViewModel() + + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) return + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = state.localizer.localize("APP.GENERAL.FREE_COLLATERAL"), + style = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + + Spacer(modifier = Modifier.weight(0.1f)) + + PlatformAmountChange( + modifier = Modifier.weight(1f), + before = if (state.before != null) { { + AmountText.Content( + state = state.before, + textStyle = TextStyle.dydxDefault + .themeFont(fontType = ThemeFont.FontType.number, fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } } else { + null + }, + after = if (state.after != null) { { + AmountText.Content( + state = state.after, + textStyle = TextStyle.dydxDefault + .themeFont(fontType = ThemeFont.FontType.number, fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_primary), + ) + } } else { + null + }, + direction = PlatformDirection.from(state.before?.amount, state.after?.amount), + textStyle = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } + } +} diff --git a/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/liquidationprice/DydxReceiptLiquidationPriceViewModel.kt b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/liquidationprice/DydxReceiptLiquidationPriceViewModel.kt new file mode 100644 index 00000000..a5cece7a --- /dev/null +++ b/v4/feature/receipt/src/main/java/exchange/dydx/trading/feature/receipt/components/liquidationprice/DydxReceiptLiquidationPriceViewModel.kt @@ -0,0 +1,120 @@ +package exchange.dydx.trading.feature.receipt.components.liquidationprice + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.Subaccount +import exchange.dydx.abacus.output.SubaccountPosition +import exchange.dydx.abacus.output.input.TradeInput +import exchange.dydx.abacus.output.input.TransferInput +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.formatter.DydxFormatter +import exchange.dydx.trading.feature.receipt.ReceiptType +import exchange.dydx.trading.feature.shared.views.AmountText +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import javax.inject.Inject + +@HiltViewModel +class DydxReceiptLiquidationPriceViewModel @Inject constructor( + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, + private val formatter: DydxFormatter, + private val receiptTypeFlow: Flow<@JvmSuppressWildcards ReceiptType?>, +) : ViewModel(), DydxViewModel { + + @OptIn(ExperimentalCoroutinesApi::class) + val state: Flow = + receiptTypeFlow + .flatMapLatest { receiptType -> + when (receiptType) { + is ReceiptType.Trade -> { + combine( + abacusStateManager.state.selectedSubaccountPositions, + abacusStateManager.state.tradeInput, + ) { positions, tradeInput -> + createViewState(positions, tradeInput) + } + } + is ReceiptType.Transfer -> { + combine( + abacusStateManager.state.selectedSubaccount, + abacusStateManager.state.transferInput, + ) { subaccount, transferInput -> + createViewState(subaccount, transferInput) + } + } + else -> flowOf() + } + } + .distinctUntilChanged() + + private fun createViewState( + positions: List?, + tradeInput: TradeInput?, + ): DydxReceiptLiquidationPriceView.ViewState { + val marketId = tradeInput?.marketId ?: "ETH-USD" + val position = positions?.firstOrNull { it.id == marketId } + return DydxReceiptLiquidationPriceView.ViewState( + localizer = localizer, + before = if (position?.buyingPower?.current != null) { + AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = position?.buyingPower?.current, + tickSize = 0, + requiresPositive = true, + ) + } else { + null + }, + after = if (position?.buyingPower?.postOrder != null) { + AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = position?.buyingPower?.postOrder, + tickSize = 0, + requiresPositive = true, + ) + } else { + null + }, + ) + } + + private fun createViewState( + subaccount: Subaccount?, + transferInput: TransferInput?, + ): DydxReceiptLiquidationPriceView.ViewState { + return DydxReceiptLiquidationPriceView.ViewState( + localizer = localizer, + before = if (subaccount?.buyingPower?.current != null) { + AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = subaccount?.buyingPower?.current, + tickSize = 0, + requiresPositive = true, + ) + } else { + null + }, + after = if (subaccount?.buyingPower?.postOrder != null) { + AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = subaccount?.buyingPower?.postOrder, + tickSize = 0, + requiresPositive = true, + ) + } else { + null + }, + ) + } +} diff --git a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/views/AmountText.kt b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/views/AmountText.kt index 73a40de8..6aff8916 100644 --- a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/views/AmountText.kt +++ b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/views/AmountText.kt @@ -58,7 +58,7 @@ object AmountText { Text( modifier = modifier, - text = state.formatter.dollar(amount, state.tickSize ?: 2) ?: "0", + text = state.formatter.dollar(amount, state.tickSize ?: 2) ?: "", style = textStyle, ) } diff --git a/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/views/LeverageView.kt b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/views/LeverageView.kt new file mode 100644 index 00000000..8893aec9 --- /dev/null +++ b/v4/feature/shared/src/main/java/exchange/dydx/trading/feature/shared/views/LeverageView.kt @@ -0,0 +1,97 @@ +package exchange.dydx.trading.feature.shared.views + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.platformui.designSystem.theme.ThemeColor +import exchange.dydx.platformui.designSystem.theme.dydxDefault +import exchange.dydx.platformui.designSystem.theme.themeColor +import exchange.dydx.trading.common.formatter.DydxFormatter +import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface +import exchange.dydx.trading.common.theme.MockLocalizer + +@Preview +@Composable +fun Preview_LeverageView() { + DydxThemedPreviewSurface { + LeverageView.Content(Modifier, LeverageView.ViewState.preview) + } +} + +object LeverageView { + + enum class DisplayOption { + IconOnly, IconAndValue + } + + data class ViewState( + val localizer: LocalizerProtocol, + val formatter: DydxFormatter, + val leverage: Double = 3.0, + val margin: Double? = null, + ) { + companion object { + val preview = ViewState( + localizer = MockLocalizer(), + formatter = DydxFormatter(), + ) + } + } + + @Composable + fun Content( + modifier: Modifier = Modifier, + state: ViewState?, + textStyle: TextStyle = TextStyle.dydxDefault, + ) { + if (state == null) return + + val leverageText = state.formatter.leverage(state.leverage) + val leverageIcon = if (state.margin != null) { + LeverageRiskView.ViewState( + localizer = state.localizer, + level = LeverageRiskView.Level.createFromMarginUsage(state.margin), + ) + } else { + null + } + + Row( + verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, + ) { + if (leverageIcon != null) { + LeverageRiskView.Content( + modifier = Modifier, + state = leverageIcon.copy( + displayOption = LeverageRiskView.DisplayOption.IconOnly, + viewSize = 14.dp, + ), + ) + Spacer(modifier = Modifier.width(6.dp)) + } + CreateValueText(Modifier, leverageText) + } + } + + @Composable + private fun CreateValueText( + modifier: Modifier, + value: String?, + ) { + Text( + text = value ?: "-", + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_primary), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/DydxAdjustMarginInputView.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/DydxAdjustMarginInputView.kt index 4d52c8f1..56caf9d7 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/DydxAdjustMarginInputView.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/DydxAdjustMarginInputView.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions @@ -27,6 +28,8 @@ import androidx.hilt.navigation.compose.hiltViewModel import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.platformui.components.buttons.PlatformPillItem import exchange.dydx.platformui.components.changes.PlatformAmountChange +import exchange.dydx.platformui.components.changes.PlatformDirection +import exchange.dydx.platformui.components.changes.PlatformDirectionArrow import exchange.dydx.platformui.components.dividers.PlatformDivider import exchange.dydx.platformui.components.inputs.PlatformTextInput import exchange.dydx.platformui.components.tabgroups.PlatformTabGroup @@ -42,9 +45,15 @@ import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle import exchange.dydx.trading.common.formatter.DydxFormatter import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface import exchange.dydx.trading.common.theme.MockLocalizer +import exchange.dydx.trading.feature.receipt.DydxReceiptView +import exchange.dydx.trading.feature.receipt.components.buyingpower.DydxReceiptFreeCollateralView +import exchange.dydx.trading.feature.receipt.components.liquidationprice.DydxReceiptLiquidationPriceView +import exchange.dydx.trading.feature.receipt.components.marginusage.DydxReceiptMarginUsageView import exchange.dydx.trading.feature.shared.scaffolds.InputFieldScaffold +import exchange.dydx.trading.feature.shared.views.AmountText import exchange.dydx.trading.feature.shared.views.HeaderViewCloseBotton -import exchange.dydx.trading.feature.shared.views.SizeTextView +import exchange.dydx.trading.feature.shared.views.MarginUsageView +import exchange.dydx.trading.feature.trade.margin.components.DydxAdjustMarginCtaButton @Preview @Composable @@ -65,15 +74,14 @@ object DydxAdjustMarginInputView : DydxComponent { val percentage: Double, ) - data class SubaccountReceipt( - val freeCollateral: List, - val marginUsage: List, + data class CrossMarginReceipt( + val freeCollateral: DydxReceiptFreeCollateralView.ViewState, + val marginUsage: DydxReceiptMarginUsageView.ViewState, ) - data class PositionReceipt( - val freeCollateral: List, - val leverage: List, - val liquidationPrice: List, + data class IsolatedMarginReceipt( + val liquidationPrice: DydxReceiptLiquidationPriceView.ViewState, + val receipts: DydxReceiptView.ViewState, ) data class ViewState( @@ -83,8 +91,8 @@ object DydxAdjustMarginInputView : DydxComponent { val percentage: Double?, val percentageOptions: List, val amountText: String?, - val subaccountReceipt: SubaccountReceipt, - val positionReceipt: PositionReceipt, + val crossMarginReceipt: CrossMarginReceipt, + val isolatedMarginReceipt: IsolatedMarginReceipt, val error: String?, val marginDirectionAction: ((direction: MarginDirection) -> Unit) = {}, val percentageAction: (() -> Unit) = {}, @@ -105,14 +113,19 @@ object DydxAdjustMarginInputView : DydxComponent { PercentageOption("50%", 0.5), ), amountText = "500", - subaccountReceipt = SubaccountReceipt( - freeCollateral = listOf("1000.00", "500.00"), - marginUsage = listOf("19.34", "38.45"), + crossMarginReceipt = CrossMarginReceipt( + freeCollateral = DydxReceiptFreeCollateralView.ViewState.preview, + marginUsage = DydxReceiptMarginUsageView.ViewState.preview, ), - positionReceipt = PositionReceipt( - freeCollateral = listOf("1000.00", "1500.00"), - leverage = listOf("3.1", "2.4"), - liquidationPrice = listOf("1200.00", "1000.00"), + isolatedMarginReceipt = IsolatedMarginReceipt( + liquidationPrice = DydxReceiptLiquidationPriceView.ViewState.preview, + receipts = DydxReceiptView.ViewState( + localizer = MockLocalizer(), + lineTypes = listOf( + DydxReceiptView.ReceiptLineType.FreeCollateral, + DydxReceiptView.ReceiptLineType.MarginUsage, + ), + ), ), error = null, ) @@ -161,22 +174,22 @@ object DydxAdjustMarginInputView : DydxComponent { state = state, ) Spacer(modifier = Modifier.weight(1f)) -// if (state.error == null) { -// LiquidationPrice( -// modifier = Modifier, -// state = state, -// ) -// } else { -// Error( -// modifier = Modifier, -// error = state.error, -// ) -// } -// Spacer(modifier = Modifier.height(8.dp)) -// PositionReceiptAndButton( -// modifier = Modifier, -// state = state, -// ) + if (state.error == null) { + LiquidationPrice( + modifier = Modifier, + state = state, + ) + } else { + Error( + modifier = Modifier, + error = state.error, + ) + } + Spacer(modifier = Modifier.height(8.dp)) + PositionReceiptAndButton( + modifier = Modifier, + state = state, + ) } } @@ -210,7 +223,10 @@ object DydxAdjustMarginInputView : DydxComponent { } } - private fun marginDirectionText(direction: MarginDirection, localizer: LocalizerProtocol): String { + private fun marginDirectionText( + direction: MarginDirection, + localizer: LocalizerProtocol + ): String { return when (direction) { MarginDirection.Add -> localizer.localize("APP.TRADE.ADD_MARGIN") MarginDirection.Remove -> localizer.localize("APP.TRADE.REMOVE_MARGIN") @@ -289,7 +305,7 @@ object DydxAdjustMarginInputView : DydxComponent { state: ViewState, ) { PlatformTabGroup( - modifier = Modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth(), scrollingEnabled = false, items = state.percentageOptions.map { { modifier -> @@ -435,28 +451,44 @@ object DydxAdjustMarginInputView : DydxComponent { modifier = modifier, ) { PlatformAmountChange( - before = { - SizeTextView.Content( - modifier = Modifier, - state = SizeTextView.ViewState( - localizer = state.localizer, - formatter = state.formatter, - size = state.subaccountReceipt.freeCollateral.firstOrNull()?.toDoubleOrNull(), - stepSize = 2, - ), - ) + modifier = Modifier.weight(1f), + before = if (state.crossMarginReceipt.freeCollateral.before != null) { + { + AmountText.Content( + state = state.crossMarginReceipt.freeCollateral.before, + textStyle = TextStyle.dydxDefault + .themeFont( + fontType = ThemeFont.FontType.number, + fontSize = ThemeFont.FontSize.small, + ) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } + } else { + null }, - after = { - SizeTextView.Content( - modifier = Modifier, - state = SizeTextView.ViewState( - localizer = state.localizer, - formatter = state.formatter, - size = state.subaccountReceipt.freeCollateral.lastOrNull()?.toDoubleOrNull(), - stepSize = 2, - ), - ) + after = if (state.crossMarginReceipt.freeCollateral.after != null) { + { + AmountText.Content( + state = state.crossMarginReceipt.freeCollateral.after, + textStyle = TextStyle.dydxDefault + .themeFont( + fontType = ThemeFont.FontType.number, + fontSize = ThemeFont.FontSize.small, + ) + .themeColor(ThemeColor.SemanticColor.text_primary), + ) + } + } else { + null }, + direction = PlatformDirection.from( + state.crossMarginReceipt.freeCollateral.before?.amount, + state.crossMarginReceipt.freeCollateral.after?.amount, + ), + textStyle = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_tertiary), ) } } @@ -492,28 +524,148 @@ object DydxAdjustMarginInputView : DydxComponent { modifier = modifier, ) { PlatformAmountChange( - before = { - SizeTextView.Content( - modifier = Modifier, - state = SizeTextView.ViewState( - localizer = state.localizer, + modifier = Modifier.weight(1f), + before = if (state.crossMarginReceipt.marginUsage.before != null) { + { + MarginUsageView.Content( + state = state.crossMarginReceipt.marginUsage.before, formatter = state.formatter, - size = state.subaccountReceipt.marginUsage.firstOrNull()?.toDoubleOrNull(), - stepSize = 2, - ), - ) + textStyle = TextStyle.dydxDefault + .themeFont( + fontSize = ThemeFont.FontSize.small, + fontType = ThemeFont.FontType.number, + ) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } + } else { + null }, - after = { - SizeTextView.Content( - modifier = Modifier, - state = SizeTextView.ViewState( - localizer = state.localizer, + after = + if (state.crossMarginReceipt.marginUsage.after != null) { + { + MarginUsageView.Content( + state = state.crossMarginReceipt.marginUsage.after, formatter = state.formatter, - size = state.subaccountReceipt.marginUsage.lastOrNull()?.toDoubleOrNull(), - stepSize = 2, - ), - ) + textStyle = TextStyle.dydxDefault + .themeFont( + fontSize = ThemeFont.FontSize.small, + fontType = ThemeFont.FontType.number, + ) + .themeColor(ThemeColor.SemanticColor.text_primary), + ) + } + } else { + null }, + direction = PlatformDirection.from( + state.crossMarginReceipt.marginUsage.after?.percent, + state.crossMarginReceipt.marginUsage.before?.percent, + ), + textStyle = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } + } + + @Composable + private fun LiquidationPrice( + modifier: Modifier, + state: ViewState, + ) { + val shape = RoundedCornerShape(8.dp) + Row( + modifier = modifier + .fillMaxWidth() + .height(80.dp) + .background(color = ThemeColor.SemanticColor.layer_5.color, shape = shape) + .padding(horizontal = ThemeShapes.HorizontalPadding) + .padding(vertical = ThemeShapes.VerticalPadding), + ) { + Column( + modifier = Modifier, + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Spacer(modifier = Modifier.weight(1f)) + Text( + text = state.localizer.localize("APP.GENERAL.ESTIMATED"), + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_tertiary) + .themeFont(fontSize = ThemeFont.FontSize.small), + ) + Text( + text = state.localizer.localize("APP.TRADE.LIQUIDATION_PRICE"), + style = TextStyle.dydxDefault + .themeColor(ThemeColor.SemanticColor.text_secondary) + .themeFont(fontSize = ThemeFont.FontSize.small), + ) + + Spacer(modifier = Modifier.weight(1f)) + } + + Spacer(modifier = Modifier.weight(1f)) + Column( + modifier = Modifier, + verticalArrangement = Arrangement.spacedBy(2.dp), + horizontalAlignment = Alignment.End, + ) { + Spacer(modifier = Modifier.weight(1f)) + if (state.isolatedMarginReceipt.liquidationPrice.before != null) { + AmountText.Content( + state = state.isolatedMarginReceipt.liquidationPrice.before, + textStyle = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.small, fontType = ThemeFont.FontType.number) + .themeColor(ThemeColor.SemanticColor.text_tertiary), + ) + } + Row( + modifier = Modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + PlatformDirectionArrow( + direction = PlatformDirection.None, + modifier = Modifier.size(12.dp), + ) + AmountText.Content( + state = state.isolatedMarginReceipt.liquidationPrice.after, + textStyle = TextStyle.dydxDefault + .themeFont(fontSize = ThemeFont.FontSize.medium, fontType = ThemeFont.FontType.number) + .themeColor(ThemeColor.SemanticColor.text_primary), + ) + } + Spacer(modifier = Modifier.weight(1f)) + } + } + } + + @Composable + private fun Error( + modifier: Modifier, + error: String, + ) { + // TODO, implement this + } + + @Composable + private fun PositionReceiptAndButton( + modifier: Modifier, + state: ViewState, + ) { + Column( + modifier = modifier + .fillMaxWidth(), + ) { + DydxReceiptView.Content( + modifier = Modifier + .offset(y = ThemeShapes.VerticalPadding), + state = state.isolatedMarginReceipt.receipts, + ) + DydxAdjustMarginCtaButton.Content( + Modifier + .fillMaxWidth() + .padding(bottom = ThemeShapes.VerticalPadding * 2), ) } } diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/DydxAdjustMarginInputViewModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/DydxAdjustMarginInputViewModel.kt index f657cda6..89176c9b 100644 --- a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/DydxAdjustMarginInputViewModel.kt +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/DydxAdjustMarginInputViewModel.kt @@ -1,7 +1,10 @@ package exchange.dydx.trading.feature.trade.margin +import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.Subaccount +import exchange.dydx.abacus.output.SubaccountPosition import exchange.dydx.abacus.output.input.TradeInput import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol @@ -9,9 +12,15 @@ import exchange.dydx.platformui.components.PlatformInfo import exchange.dydx.trading.common.DydxViewModel import exchange.dydx.trading.common.formatter.DydxFormatter import exchange.dydx.trading.common.navigation.DydxRouter +import exchange.dydx.trading.feature.receipt.DydxReceiptView +import exchange.dydx.trading.feature.receipt.components.buyingpower.DydxReceiptFreeCollateralView +import exchange.dydx.trading.feature.receipt.components.liquidationprice.DydxReceiptLiquidationPriceView +import exchange.dydx.trading.feature.receipt.components.marginusage.DydxReceiptMarginUsageView +import exchange.dydx.trading.feature.shared.views.AmountText +import exchange.dydx.trading.feature.shared.views.MarginUsageView import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map import javax.inject.Inject @HiltViewModel @@ -23,13 +32,23 @@ class DydxAdjustMarginInputViewModel @Inject constructor( val platformInfo: PlatformInfo, ) : ViewModel(), DydxViewModel { - val state: Flow = abacusStateManager.state.tradeInput - .map { - createViewState(it) + val state: Flow = + combine( + abacusStateManager.state.tradeInput, + abacusStateManager.state.selectedSubaccount, + abacusStateManager.state.selectedSubaccountPositions, + ) { tradeInput, subaccount, positions -> + createViewState(tradeInput, subaccount, positions) } - .distinctUntilChanged() + .distinctUntilChanged() + + private fun createViewState( + tradeInput: TradeInput?, + subaccount: Subaccount?, + positions: List?, + ): DydxAdjustMarginInputView.ViewState { + val isolatedMargin = positions?.firstOrNull() - private fun createViewState(tradeInput: TradeInput?): DydxAdjustMarginInputView.ViewState { /* Abacus not implemented for adjust margin yet. This is a placeholder. */ @@ -45,14 +64,62 @@ class DydxAdjustMarginInputViewModel @Inject constructor( DydxAdjustMarginInputView.PercentageOption("50%", 0.5), ), amountText = "500", - subaccountReceipt = DydxAdjustMarginInputView.SubaccountReceipt( - freeCollateral = listOf("1000.00", "500.00"), - marginUsage = listOf("19.34", "38.45"), + crossMarginReceipt = DydxAdjustMarginInputView.CrossMarginReceipt( + freeCollateral = DydxReceiptFreeCollateralView.ViewState( + localizer = localizer, + before = AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = subaccount?.freeCollateral?.current, + tickSize = 2, + ), + after = AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = subaccount?.freeCollateral?.postOrder, + tickSize = 2, + ), + ), + marginUsage = DydxReceiptMarginUsageView.ViewState( + localizer = localizer, + formatter = formatter, + before = MarginUsageView.ViewState( + localizer = localizer, + displayOption = MarginUsageView.DisplayOption.IconAndValue, + percent = subaccount?.marginUsage?.current ?: 0.5, + ), + after = MarginUsageView.ViewState( + localizer = localizer, + displayOption = MarginUsageView.DisplayOption.IconAndValue, + percent = subaccount?.marginUsage?.current ?: 0.5, + ), + ), ), - positionReceipt = DydxAdjustMarginInputView.PositionReceipt( - freeCollateral = listOf("1000.00", "1500.00"), - leverage = listOf("3.1", "2.4"), - liquidationPrice = listOf("1200.00", "1000.00"), + isolatedMarginReceipt = DydxAdjustMarginInputView.IsolatedMarginReceipt( + liquidationPrice = DydxReceiptLiquidationPriceView.ViewState( + localizer = localizer, + before = AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = isolatedMargin?.liquidationPrice?.current, + tickSize = 2, + ), + after = AmountText.ViewState( + localizer = localizer, + formatter = formatter, + amount = isolatedMargin?.liquidationPrice?.postOrder, + tickSize = 2, + ), + ), + receipts = DydxReceiptView.ViewState( + localizer = localizer, + height = 128.dp, + padding = 0.dp, + lineTypes = listOf( + DydxReceiptView.ReceiptLineType.IsolatedPositionMarginUsage, + DydxReceiptView.ReceiptLineType.IsolatedPositionLeverage, + ), + ), ), error = null, marginDirectionAction = { }, diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/components/DydxAdjustMarginCtaButton.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/components/DydxAdjustMarginCtaButton.kt new file mode 100644 index 00000000..529159c5 --- /dev/null +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/components/DydxAdjustMarginCtaButton.kt @@ -0,0 +1,50 @@ +package exchange.dydx.trading.feature.trade.margin.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import exchange.dydx.trading.common.component.DydxComponent +import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle +import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface +import exchange.dydx.trading.feature.shared.views.InputCtaButton + +@Preview +@Composable +fun Preview_DydxAdjustMarginCtaButton() { + DydxThemedPreviewSurface { + DydxAdjustMarginCtaButton.Content(Modifier, DydxAdjustMarginCtaButton.ViewState.preview) + } +} + +object DydxAdjustMarginCtaButton : DydxComponent { + data class ViewState( + val ctaButton: InputCtaButton.ViewState? = null, + ) { + companion object { + val preview = ViewState( + ctaButton = InputCtaButton.ViewState.preview, + ) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val viewModel: DydxAdjustMarginCtaButtonModel = hiltViewModel() + + val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value + Content(modifier, state) + } + + @Composable + fun Content(modifier: Modifier, state: ViewState?) { + if (state == null) { + return + } + + InputCtaButton.Content( + modifier = modifier, + state = state.ctaButton, + ) + } +} diff --git a/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/components/DydxAdjustMarginCtaButtonModel.kt b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/components/DydxAdjustMarginCtaButtonModel.kt new file mode 100644 index 00000000..eda45451 --- /dev/null +++ b/v4/feature/trade/src/main/java/exchange/dydx/trading/feature/trade/margin/components/DydxAdjustMarginCtaButtonModel.kt @@ -0,0 +1,54 @@ +package exchange.dydx.trading.feature.trade.margin.components + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.TradeInput +import exchange.dydx.abacus.output.input.ValidationError +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol +import exchange.dydx.trading.common.AppConfig +import exchange.dydx.trading.common.DydxViewModel +import exchange.dydx.trading.common.theme.DydxTheme +import exchange.dydx.trading.feature.shared.views.InputCtaButton +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import javax.inject.Inject + +@HiltViewModel +class DydxAdjustMarginCtaButtonModel @Inject constructor( + private val appConfig: AppConfig, + private val theme: DydxTheme, + private val localizer: LocalizerProtocol, + private val abacusStateManager: AbacusStateManagerProtocol, +) : ViewModel(), DydxViewModel { + val state: Flow = + combine( + abacusStateManager.state.tradeInput, + abacusStateManager.state.validationErrors, + ) { tradeInput, validationErrors -> + createViewState(tradeInput, validationErrors) + } + .distinctUntilChanged() + + private fun createViewState( + tradeInput: TradeInput?, + validationErrors: List, + ): DydxAdjustMarginCtaButton.ViewState { + val firstBlockingError = + validationErrors.firstOrNull { it.type == ErrorType.required || it.type == ErrorType.error } + + return DydxAdjustMarginCtaButton.ViewState( + ctaButton = InputCtaButton.ViewState( + localizer = localizer, + ctaButtonState = InputCtaButton.State.Enabled( + localizer.localize("APP.TRADE.ADD_MARGIN"), + ), + ctaAction = { + // TODO, Submit the orders + }, + ), + ) + } +}